Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54d06e580d | ||
|
|
7b1d23eba4 | ||
|
|
2502c279fc | ||
|
|
878fc8c2fd | ||
|
|
64902eebcb | ||
|
|
e520f6afc4 | ||
|
|
0cc1ae9324 | ||
|
|
bd4fde9e63 | ||
|
|
c85cfa90a4 | ||
|
|
a0449b50c8 | ||
|
|
394b063b8c | ||
|
|
b62d84424e | ||
|
|
20b84a9459 | ||
|
|
dafc68f049 | ||
|
|
4745fc0db1 | ||
|
|
b009dfe4ee | ||
|
|
eba94f430c | ||
|
|
06347b7e3c | ||
|
|
6bc426b21d | ||
|
|
4a849aab7f | ||
|
|
ad2d0c84e4 | ||
|
|
6bf360fbfb | ||
|
|
380292a8aa | ||
|
|
f9949da745 | ||
|
|
b0cc52e0aa | ||
|
|
9ef705650b | ||
|
|
c6dadbe75e | ||
|
|
21026340ad | ||
|
|
541c8c4b93 | ||
|
|
b97373bf37 | ||
|
|
c0b561cb5a | ||
|
|
bd3ccfd0a9 | ||
|
|
5470acd3af | ||
|
|
64696fe973 | ||
|
|
ed2fa4cdd8 | ||
|
|
03cca1d1c0 | ||
|
|
354b075885 | ||
|
|
de7f039b99 | ||
|
|
19797d5d23 | ||
|
|
144c2b5fc7 | ||
|
|
7da40d1c2e | ||
|
|
4e3a5ca33b | ||
|
|
97e4707f72 | ||
|
|
450ee43677 | ||
|
|
c0b2e886d2 | ||
|
|
bb4749c65b | ||
|
|
5aa37c68a2 | ||
|
|
6e5a1a1f4d | ||
|
|
b624ce16c3 | ||
|
|
3be5b5f919 | ||
|
|
04ac5b69ac | ||
|
|
9cc14ac5c7 | ||
|
|
755eb1405b | ||
|
|
0138294b36 | ||
|
|
c0a4addf30 | ||
|
|
39bbee0eeb | ||
|
|
39dda3f534 | ||
|
|
1cb8dbe795 | ||
|
|
e13820bbf0 | ||
|
|
3973362457 | ||
|
|
b38877e193 | ||
|
|
0e1dbb5377 | ||
|
|
f07cc8b103 | ||
|
|
67775bc797 | ||
|
|
05933a5f0c | ||
|
|
d32b2ca524 | ||
|
|
d0a895628e | ||
|
|
90f2b3fb21 | ||
|
|
0f95800dde | ||
|
|
04971f8672 | ||
|
|
b6177b27f4 | ||
|
|
ae97e87506 | ||
|
|
2d73f541c0 | ||
|
|
7561eabe52 | ||
|
|
067fb1b0b7 | ||
|
|
78071841cc | ||
|
|
fac232229e | ||
|
|
a43021e87c | ||
|
|
1a0de3b3cf | ||
|
|
947154dc9c | ||
|
|
8acd1f1fe4 | ||
|
|
665210e506 | ||
|
|
91b9370a21 | ||
|
|
ca0aa600b4 | ||
|
|
1c42c4c969 | ||
|
|
6e406311c3 | ||
|
|
87884c1af2 | ||
|
|
c0623a949c | ||
|
|
42a23ea23c | ||
|
|
710c31d1ca | ||
|
|
f637ba4dad | ||
|
|
80d2c60cef | ||
|
|
b17d71e4c3 | ||
|
|
3c5bd3a605 | ||
|
|
7cc8d1e693 | ||
|
|
99520d54a5 | ||
|
|
e10a8c078b | ||
|
|
7a196fed7c | ||
|
|
c418c7e725 | ||
|
|
e35748deff | ||
|
|
b57efbe31c | ||
|
|
8f91437701 | ||
|
|
a7eb547ca4 | ||
|
|
0ceb12f74f | ||
|
|
4960624de7 | ||
|
|
2c92f24d89 | ||
|
|
0b7c679dba | ||
|
|
aab3cdc58a | ||
|
|
3c63f5b204 | ||
|
|
0b97bbf827 | ||
|
|
970dbb0c59 | ||
|
|
d21d3c2ceb | ||
|
|
0f6493b8ce | ||
|
|
f26beec1d3 | ||
|
|
c4225e29ed | ||
|
|
9243326cd3 | ||
|
|
86beb8023a | ||
|
|
f4e134742b | ||
|
|
2b17bf4114 | ||
|
|
c4f0fc8f7b | ||
|
|
3d6c836e7d | ||
|
|
c279055af4 | ||
|
|
657cba042a | ||
|
|
7a58e48cae | ||
|
|
670eab62bf | ||
|
|
fc85586809 | ||
|
|
837fc4988d | ||
|
|
4664b47851 | ||
|
|
9864d932e0 | ||
|
|
436f60caa9 | ||
|
|
e9530eda9d | ||
|
|
f4ddac16f9 | ||
|
|
33bdc07f55 | ||
|
|
130a4ed2d3 | ||
|
|
59ebc1af8a | ||
|
|
82e15d2e98 | ||
|
|
9c842f1895 | ||
|
|
8cc849488b | ||
|
|
fcaa232967 | ||
|
|
d3f8153b90 | ||
|
|
99ecf22790 | ||
|
|
b1b7ee65b4 | ||
|
|
6889456662 | ||
|
|
081553dc9f | ||
|
|
5d8d796807 | ||
|
|
f3d88fc746 | ||
|
|
d87ccae6c9 | ||
|
|
2760a7816d | ||
|
|
8f5c649d0f | ||
|
|
fcf9e7a1ef | ||
|
|
2be7b6480a | ||
|
|
f5ded86c02 | ||
|
|
8abfaf0ffd | ||
|
|
b070ba142f | ||
|
|
bd7c6828bf | ||
|
|
d0671ec58c | ||
|
|
1e40ffd1cc | ||
|
|
da2c3e9ed7 | ||
|
|
f62d6abb69 | ||
|
|
12ad7a6322 | ||
|
|
b75247ea3a | ||
|
|
9a963bbe79 | ||
|
|
994bc8b0e4 | ||
|
|
2451605033 | ||
|
|
10942527f3 | ||
|
|
4909ed5ccd | ||
|
|
5cf5df68dc | ||
|
|
dd32cc99ea | ||
|
|
79092dc8eb | ||
|
|
6229e4610a | ||
|
|
bfe36d3f4a | ||
|
|
d42bf44fad | ||
|
|
33e352819c | ||
|
|
53ee0aaee1 | ||
|
|
42707a19bd | ||
|
|
0888706790 | ||
|
|
16453a05f8 | ||
|
|
2fbb7466d3 | ||
|
|
f29f94f45f | ||
|
|
cd973868fc | ||
|
|
3c35f02cac | ||
|
|
22c93e2389 | ||
|
|
8c751eb532 | ||
|
|
4df443e007 | ||
|
|
f52fa41439 | ||
|
|
f77d72fd86 | ||
|
|
9b80c84794 | ||
|
|
725fc658f8 | ||
|
|
e4e27662f5 | ||
|
|
83474da7b5 | ||
|
|
9146e5f287 | ||
|
|
ff4502c63a | ||
|
|
9711bd8fe1 | ||
|
|
05139e53be | ||
|
|
870b2642a9 | ||
|
|
d31b26ae7d | ||
|
|
5511925ba2 | ||
|
|
f96b20717d | ||
|
|
940c9c45d7 | ||
|
|
87d6008dfc | ||
|
|
b9c0c8d2dc | ||
|
|
81c30d5fd5 | ||
|
|
0aa33d88a5 | ||
|
|
e64a504bb1 | ||
|
|
bc876a159e | ||
|
|
4aa1a838ed | ||
|
|
095a51edd0 | ||
|
|
a3a11bdf3f | ||
|
|
70b503f3d4 | ||
|
|
bbf609b880 | ||
|
|
0992bafe30 | ||
|
|
c810c5275a | ||
|
|
3c1b06872d | ||
|
|
cefdd2f66c | ||
|
|
5dac13b1da | ||
|
|
8aebf48193 | ||
|
|
fbb905957b | ||
|
|
e0ce135838 | ||
|
|
60497c60c1 | ||
|
|
251a77c8b4 | ||
|
|
7b7494b8a4 | ||
|
|
9390dcdd43 | ||
|
|
e6fb460071 | ||
|
|
6137fdeb33 | ||
|
|
4a4d02ea6a | ||
|
|
be26e5f152 | ||
|
|
127bfba135 | ||
|
|
7efae3c125 | ||
|
|
1e5af21000 | ||
|
|
33a0a4c173 | ||
|
|
eeb7974e05 | ||
|
|
f45ea1a31c | ||
|
|
a866dbaa80 | ||
|
|
62447d6b89 | ||
|
|
93f0724b83 | ||
|
|
724762843d | ||
|
|
ff16afbf0b | ||
|
|
9d7ef25062 | ||
|
|
88078d65e9 | ||
|
|
b07a97c17e | ||
|
|
41e7d65e2a | ||
|
|
7fa5865cf6 | ||
|
|
e0b8fe3b1a | ||
|
|
4a11dd1e16 | ||
|
|
34a474101f | ||
|
|
e6799e7a04 | ||
|
|
0f83f9992c | ||
|
|
d2ad78eb40 | ||
|
|
0b32738f4e | ||
|
|
a9cedb3fca | ||
|
|
51f9cd4bb4 | ||
|
|
a1668e2411 | ||
|
|
b4324cd685 | ||
|
|
6fe4ed3e24 | ||
|
|
bc8bdfe385 | ||
|
|
ee28e3346b | ||
|
|
0cf4c7b7b7 | ||
|
|
123493ee59 | ||
|
|
2d498dd138 | ||
|
|
2343c79126 | ||
|
|
09a5a69f86 | ||
|
|
65929c02bc | ||
|
|
22466d6b98 | ||
|
|
23fe79c618 | ||
|
|
cb7727900c | ||
|
|
bdf6052388 | ||
|
|
99d653eece |
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -10,6 +10,7 @@ env/
|
|||||||
venv/
|
venv/
|
||||||
eggs/
|
eggs/
|
||||||
dist/
|
dist/
|
||||||
|
executable/
|
||||||
build/
|
build/
|
||||||
vendor/
|
vendor/
|
||||||
.eggs/
|
.eggs/
|
||||||
@@ -29,4 +30,4 @@ vendor/
|
|||||||
settings.yaml
|
settings.yaml
|
||||||
gdrive_credentials
|
gdrive_credentials
|
||||||
client_secrets.json
|
client_secrets.json
|
||||||
|
gmail.json
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
|
Calibre-Web is a web app providing a clean interface for browsing, reading and downloading eBooks using an existing [Calibre](https://calibre-ebook.com) database.
|
||||||
|
|
||||||
|
   [](https://discord.gg/h2VsJ2NEfB)
|
||||||
|
|
||||||
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
|
*This software is a fork of [library](https://github.com/mutschler/calibreserver) and licensed under the GPL v3 License.*
|
||||||
|
|
||||||

|

|
||||||
@@ -12,7 +14,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
|||||||
- full graphical setup
|
- full graphical setup
|
||||||
- User management with fine-grained per-user permissions
|
- User management with fine-grained per-user permissions
|
||||||
- Admin interface
|
- Admin interface
|
||||||
- User Interface in czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, turkish, ukrainian
|
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified chinese, spanish, swedish, turkish, ukrainian
|
||||||
- OPDS feed for eBook reader apps
|
- OPDS feed for eBook reader apps
|
||||||
- Filter and search by titles, authors, tags, series and language
|
- Filter and search by titles, authors, tags, series and language
|
||||||
- Create a custom book collection (shelves)
|
- Create a custom book collection (shelves)
|
||||||
@@ -22,7 +24,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
|||||||
- Support for public user registration
|
- Support for public user registration
|
||||||
- Send eBooks to Kindle devices with the click of a button
|
- Send eBooks to Kindle devices with the click of a button
|
||||||
- Sync your Kobo devices through Calibre-Web with your Calibre library
|
- Sync your Kobo devices through Calibre-Web with your Calibre library
|
||||||
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz)
|
- Support for reading eBooks directly in the browser (.txt, .epub, .pdf, .cbr, .cbt, .cbz, .djvu)
|
||||||
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
|
- Upload new books in many formats, including audio formats (.mp3, .m4a, .m4b)
|
||||||
- Support for Calibre Custom Columns
|
- Support for Calibre Custom Columns
|
||||||
- Ability to hide content based on categories and Custom Column content per user
|
- Ability to hide content based on categories and Custom Column content per user
|
||||||
@@ -80,7 +82,9 @@ Pre-built Docker images are available in these Docker Hub repositories:
|
|||||||
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
|
+ The "path to convertertool" should be set to `/usr/bin/ebook-convert`
|
||||||
+ The "path to unrar" should be set to `/usr/bin/unrar`
|
+ The "path to unrar" should be set to `/usr/bin/unrar`
|
||||||
|
|
||||||
# Wiki
|
# Contact
|
||||||
|
|
||||||
|
Just reach us out on [Discord](https://discord.gg/h2VsJ2NEfB)
|
||||||
|
|
||||||
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)
|
For further information, How To's and FAQ please check the [Wiki](https://github.com/janeczku/calibre-web/wiki)
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ def create_app():
|
|||||||
|
|
||||||
log.info('Starting Calibre Web...')
|
log.info('Starting Calibre Web...')
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
log.info('Python2 is EOL since end of 2019, this version of Calibre-Web supporting Python2 please consider upgrading to Python3')
|
log.info('Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2 please consider upgrading to Python3')
|
||||||
print('Python2 is EOL since end of 2019, this version of Calibre-Web supporting Python2 please consider upgrading to Python3')
|
print('Python2 is EOL since end of 2019, this version of Calibre-Web is no longer supporting Python2 please consider upgrading to Python3')
|
||||||
Principal(app)
|
Principal(app)
|
||||||
lm.init_app(app)
|
lm.init_app(app)
|
||||||
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
app.secret_key = os.getenv('SECRET_KEY', config_sql.get_flask_session_key(ub.session))
|
||||||
@@ -127,7 +127,7 @@ def get_locale():
|
|||||||
user = getattr(g, 'user', None)
|
user = getattr(g, 'user', None)
|
||||||
# user = None
|
# user = None
|
||||||
if user is not None and hasattr(user, "locale"):
|
if user is not None and hasattr(user, "locale"):
|
||||||
if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings
|
if user.name != 'Guest': # if the account is the guest account bypass the config lang settings
|
||||||
return user.locale
|
return user.locale
|
||||||
|
|
||||||
preferred = list()
|
preferred = list()
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
from flask_login.__about__ import __version__ as flask_loginVersion
|
from flask_login.__about__ import __version__ as flask_loginVersion
|
||||||
try:
|
try:
|
||||||
|
# pylint: disable=unused-import
|
||||||
import unidecode
|
import unidecode
|
||||||
# _() necessary to make babel aware of string for translation
|
# _() necessary to make babel aware of string for translation
|
||||||
unidecode_version = _(u'installed')
|
unidecode_version = _(u'installed')
|
||||||
@@ -48,6 +49,11 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
flask_danceVersion = None
|
flask_danceVersion = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from greenlet import __version__ as greenlet_Version
|
||||||
|
except ImportError:
|
||||||
|
greenlet_Version = None
|
||||||
|
|
||||||
from . import services
|
from . import services
|
||||||
|
|
||||||
about = flask.Blueprint('about', __name__)
|
about = flask.Blueprint('about', __name__)
|
||||||
@@ -77,7 +83,8 @@ _VERSIONS = OrderedDict(
|
|||||||
python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
|
python_LDAP = services.ldapVersion if bool(services.ldapVersion) else None,
|
||||||
Goodreads = u'installed' if bool(services.goodreads_support) else None,
|
Goodreads = u'installed' if bool(services.goodreads_support) else None,
|
||||||
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
||||||
flask_dance = flask_danceVersion
|
flask_dance = flask_danceVersion,
|
||||||
|
greenlet = greenlet_Version
|
||||||
)
|
)
|
||||||
_VERSIONS.update(uploader.get_versions())
|
_VERSIONS.update(uploader.get_versions())
|
||||||
|
|
||||||
|
|||||||
@@ -31,20 +31,22 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
from babel import Locale as LC
|
from babel import Locale as LC
|
||||||
from babel.dates import format_datetime
|
from babel.dates import format_datetime
|
||||||
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g
|
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response
|
||||||
from flask_login import login_required, current_user, logout_user, confirm_login
|
from flask_login import login_required, current_user, logout_user, confirm_login
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
|
||||||
from sqlalchemy.sql.expression import func, or_
|
from sqlalchemy.sql.expression import func, or_, text
|
||||||
|
|
||||||
from . import constants, logger, helper, services
|
from . import constants, logger, helper, services
|
||||||
from .cli import filepicker
|
from .cli import filepicker
|
||||||
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
|
from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils
|
||||||
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash
|
from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \
|
||||||
|
valid_email, check_username
|
||||||
from .gdriveutils import is_gdrive_ready, gdrive_support
|
from .gdriveutils import is_gdrive_ready, gdrive_support
|
||||||
from .render_template import render_title_template, get_sidebar_config
|
from .render_template import render_title_template, get_sidebar_config
|
||||||
from . import debug_info
|
from . import debug_info, _BABEL_TRANSLATIONS
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@@ -56,11 +58,13 @@ log = logger.create()
|
|||||||
feature_support = {
|
feature_support = {
|
||||||
'ldap': bool(services.ldap),
|
'ldap': bool(services.ldap),
|
||||||
'goodreads': bool(services.goodreads_support),
|
'goodreads': bool(services.goodreads_support),
|
||||||
'kobo': bool(services.kobo)
|
'kobo': bool(services.kobo),
|
||||||
|
'updater': constants.UPDATER_AVAILABLE,
|
||||||
|
'gmail': bool(services.gmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import rarfile
|
import rarfile # pylint: disable=unused-import
|
||||||
feature_support['rar'] = True
|
feature_support['rar'] = True
|
||||||
except (ImportError, SyntaxError):
|
except (ImportError, SyntaxError):
|
||||||
feature_support['rar'] = False
|
feature_support['rar'] = False
|
||||||
@@ -148,7 +152,7 @@ def shutdown():
|
|||||||
else:
|
else:
|
||||||
showtext['text'] = _(u'Performing shutdown of server, please close window')
|
showtext['text'] = _(u'Performing shutdown of server, please close window')
|
||||||
# stop gevent/tornado server
|
# stop gevent/tornado server
|
||||||
web_server.stop(task == 0)
|
web_server.stop(task==0)
|
||||||
return json.dumps(showtext)
|
return json.dumps(showtext)
|
||||||
|
|
||||||
if task == 2:
|
if task == 2:
|
||||||
@@ -212,6 +216,278 @@ def view_configuration():
|
|||||||
restrictColumns=restrict_columns,
|
restrictColumns=restrict_columns,
|
||||||
title=_(u"UI Configuration"), page="uiconfig")
|
title=_(u"UI Configuration"), page="uiconfig")
|
||||||
|
|
||||||
|
@admi.route("/admin/usertable")
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def edit_user_table():
|
||||||
|
visibility = current_user.view_settings.get('useredit', {})
|
||||||
|
languages = calibre_db.speaking_language()
|
||||||
|
translations = babel.list_translations() + [LC('en')]
|
||||||
|
allUser = ub.session.query(ub.User)
|
||||||
|
tags = calibre_db.session.query(db.Tags)\
|
||||||
|
.join(db.books_tags_link)\
|
||||||
|
.join(db.Books)\
|
||||||
|
.filter(calibre_db.common_filters()) \
|
||||||
|
.group_by(text('books_tags_link.tag'))\
|
||||||
|
.order_by(db.Tags.name).all()
|
||||||
|
if config.config_restricted_column:
|
||||||
|
custom_values = calibre_db.session.query(db.cc_classes[config.config_restricted_column]).all()
|
||||||
|
else:
|
||||||
|
custom_values = []
|
||||||
|
if not config.config_anonbrowse:
|
||||||
|
allUser = allUser.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS)
|
||||||
|
|
||||||
|
return render_title_template("user_table.html",
|
||||||
|
users=allUser.all(),
|
||||||
|
tags=tags,
|
||||||
|
custom_values=custom_values,
|
||||||
|
translations=translations,
|
||||||
|
languages=languages,
|
||||||
|
visiblility=visibility,
|
||||||
|
all_roles=constants.ALL_ROLES,
|
||||||
|
sidebar_settings=constants.sidebar_settings,
|
||||||
|
title=_(u"Edit Users"),
|
||||||
|
page="usertable")
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route("/ajax/listusers")
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def list_users():
|
||||||
|
off = int(request.args.get("offset") or 0)
|
||||||
|
limit = int(request.args.get("limit") or 10)
|
||||||
|
search = request.args.get("search")
|
||||||
|
sort = request.args.get("sort", "id")
|
||||||
|
order = request.args.get("order", "").lower()
|
||||||
|
state = None
|
||||||
|
if sort == "state":
|
||||||
|
state = json.loads(request.args.get("state", "[]"))
|
||||||
|
|
||||||
|
if sort != "state" and order:
|
||||||
|
order = text(sort + " " + order)
|
||||||
|
elif not state:
|
||||||
|
order = ub.User.id.asc()
|
||||||
|
|
||||||
|
all_user = ub.session.query(ub.User)
|
||||||
|
if not config.config_anonbrowse:
|
||||||
|
all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS)
|
||||||
|
|
||||||
|
total_count = filtered_count = all_user.count()
|
||||||
|
|
||||||
|
if search:
|
||||||
|
all_user = all_user.filter(or_(func.lower(ub.User.name).ilike("%" + search + "%"),
|
||||||
|
func.lower(ub.User.kindle_mail).ilike("%" + search + "%"),
|
||||||
|
func.lower(ub.User.email).ilike("%" + search + "%")))
|
||||||
|
if state:
|
||||||
|
users = calibre_db.get_checkbox_sorted(all_user.all(), state, off, limit, request.args.get("order", "").lower())
|
||||||
|
else:
|
||||||
|
users = all_user.order_by(order).offset(off).limit(limit).all()
|
||||||
|
if search:
|
||||||
|
filtered_count = len(users)
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
if user.default_language == "all":
|
||||||
|
user.default = _("All")
|
||||||
|
else:
|
||||||
|
user.default = LC.parse(user.default_language).get_language_name(get_locale())
|
||||||
|
|
||||||
|
table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users}
|
||||||
|
js_list = json.dumps(table_entries, cls=db.AlchemyEncoder)
|
||||||
|
response = make_response(js_list)
|
||||||
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
|
return response
|
||||||
|
|
||||||
|
@admi.route("/ajax/deleteuser", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def delete_user():
|
||||||
|
user_ids = request.form.to_dict(flat=False)
|
||||||
|
users = None
|
||||||
|
if "userid[]" in user_ids:
|
||||||
|
users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids['userid[]'])).all()
|
||||||
|
elif "userid" in user_ids:
|
||||||
|
users = ub.session.query(ub.User).filter(ub.User.id == user_ids['userid'][0]).all()
|
||||||
|
count = 0
|
||||||
|
errors = list()
|
||||||
|
success = list()
|
||||||
|
if not users:
|
||||||
|
log.error("User not found")
|
||||||
|
return Response(json.dumps({'type': "danger", 'message': _("User not found")}), mimetype='application/json')
|
||||||
|
for user in users:
|
||||||
|
try:
|
||||||
|
message = _delete_user(user)
|
||||||
|
count += 1
|
||||||
|
except Exception as ex:
|
||||||
|
log.error(ex)
|
||||||
|
errors.append({'type': "danger", 'message': str(ex)})
|
||||||
|
|
||||||
|
if count == 1:
|
||||||
|
log.info("User {} deleted".format(user_ids))
|
||||||
|
success = [{'type': "success", 'message': message}]
|
||||||
|
elif count > 1:
|
||||||
|
log.info("Users {} deleted".format(user_ids))
|
||||||
|
success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}]
|
||||||
|
success.extend(errors)
|
||||||
|
return Response(json.dumps(success), mimetype='application/json')
|
||||||
|
|
||||||
|
@admi.route("/ajax/getlocale")
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def table_get_locale():
|
||||||
|
locale = babel.list_translations() + [LC('en')]
|
||||||
|
ret = list()
|
||||||
|
current_locale = get_locale()
|
||||||
|
for loc in locale:
|
||||||
|
ret.append({'value': str(loc), 'text': loc.get_language_name(current_locale)})
|
||||||
|
return json.dumps(ret)
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route("/ajax/getdefaultlanguage")
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def table_get_default_lang():
|
||||||
|
languages = calibre_db.speaking_language()
|
||||||
|
ret = list()
|
||||||
|
ret.append({'value': 'all', 'text': _('Show All')})
|
||||||
|
for lang in languages:
|
||||||
|
ret.append({'value': lang.lang_code, 'text': lang.name})
|
||||||
|
return json.dumps(ret)
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route("/ajax/editlistusers/<param>", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def edit_list_user(param):
|
||||||
|
vals = request.form.to_dict(flat=False)
|
||||||
|
all_user = ub.session.query(ub.User)
|
||||||
|
if not config.config_anonbrowse:
|
||||||
|
all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS)
|
||||||
|
# only one user is posted
|
||||||
|
if "pk" in vals:
|
||||||
|
users = [all_user.filter(ub.User.id == vals['pk'][0]).one_or_none()]
|
||||||
|
else:
|
||||||
|
if "pk[]" in vals:
|
||||||
|
users = all_user.filter(ub.User.id.in_(vals['pk[]'])).all()
|
||||||
|
else:
|
||||||
|
return _("Malformed request"), 400
|
||||||
|
if 'field_index' in vals:
|
||||||
|
vals['field_index'] = vals['field_index'][0]
|
||||||
|
if 'value' in vals:
|
||||||
|
vals['value'] = vals['value'][0]
|
||||||
|
elif not ('value[]' in vals):
|
||||||
|
return _("Malformed request"), 400
|
||||||
|
for user in users:
|
||||||
|
try:
|
||||||
|
if param in ['denied_tags', 'allowed_tags', 'allowed_column_value', 'denied_column_value']:
|
||||||
|
if 'value[]' in vals:
|
||||||
|
setattr(user, param, prepare_tags(user, vals['action'][0], param, vals['value[]']))
|
||||||
|
else:
|
||||||
|
setattr(user, param, vals['value'].strip())
|
||||||
|
else:
|
||||||
|
vals['value'] = vals['value'].strip()
|
||||||
|
if param == 'name':
|
||||||
|
if user.name == "Guest":
|
||||||
|
raise Exception(_("Guest Name can't be changed"))
|
||||||
|
user.name = check_username(vals['value'])
|
||||||
|
elif param =='email':
|
||||||
|
user.email = check_email(vals['value'])
|
||||||
|
elif param == 'kindle_mail':
|
||||||
|
user.kindle_mail = valid_email(vals['value']) if vals['value'] else ""
|
||||||
|
elif param.endswith('role'):
|
||||||
|
value = int(vals['field_index'])
|
||||||
|
if user.name == "Guest" and value in \
|
||||||
|
[constants.ROLE_ADMIN, constants.ROLE_PASSWD, constants.ROLE_EDIT_SHELFS]:
|
||||||
|
raise Exception(_("Guest can't have this role"))
|
||||||
|
# check for valid value, last on checks for power of 2 value
|
||||||
|
if value > 0 and value <= constants.ROLE_VIEWER and (value & value-1 == 0 or value == 1):
|
||||||
|
if vals['value'] == 'true':
|
||||||
|
user.role |= value
|
||||||
|
elif vals['value'] == 'false':
|
||||||
|
if value == constants.ROLE_ADMIN:
|
||||||
|
if not ub.session.query(ub.User).\
|
||||||
|
filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
||||||
|
ub.User.id != user.id).count():
|
||||||
|
return Response(
|
||||||
|
json.dumps([{'type': "danger",
|
||||||
|
'message':_(u"No admin user remaining, can't remove admin role",
|
||||||
|
nick=user.name)}]), mimetype='application/json')
|
||||||
|
user.role &= ~value
|
||||||
|
else:
|
||||||
|
raise Exception(_("Value has to be true or false"))
|
||||||
|
else:
|
||||||
|
raise Exception(_("Invalid role"))
|
||||||
|
elif param.startswith('sidebar'):
|
||||||
|
value = int(vals['field_index'])
|
||||||
|
if user.name == "Guest" and value == constants.SIDEBAR_READ_AND_UNREAD:
|
||||||
|
raise Exception(_("Guest can't have this view"))
|
||||||
|
# check for valid value, last on checks for power of 2 value
|
||||||
|
if value > 0 and value <= constants.SIDEBAR_LIST and (value & value-1 == 0 or value == 1):
|
||||||
|
if vals['value'] == 'true':
|
||||||
|
user.sidebar_view |= value
|
||||||
|
elif vals['value'] == 'false':
|
||||||
|
user.sidebar_view &= ~value
|
||||||
|
else:
|
||||||
|
raise Exception(_("Value has to be true or false"))
|
||||||
|
else:
|
||||||
|
raise Exception(_("Invalid view"))
|
||||||
|
elif param == 'locale':
|
||||||
|
if user.name == "Guest":
|
||||||
|
raise Exception(_("Guest's Locale is determined automatically and can't be set"))
|
||||||
|
if vals['value'] in _BABEL_TRANSLATIONS:
|
||||||
|
user.locale = vals['value']
|
||||||
|
else:
|
||||||
|
raise Exception(_("No Valid Locale Given"))
|
||||||
|
elif param == 'default_language':
|
||||||
|
languages = calibre_db.session.query(db.Languages) \
|
||||||
|
.join(db.books_languages_link) \
|
||||||
|
.join(db.Books) \
|
||||||
|
.filter(calibre_db.common_filters()) \
|
||||||
|
.group_by(text('books_languages_link.lang_code')).all()
|
||||||
|
lang_codes = [lang.lang_code for lang in languages] + ["all"]
|
||||||
|
if vals['value'] in lang_codes:
|
||||||
|
user.default_language = vals['value']
|
||||||
|
else:
|
||||||
|
raise Exception(_("No Valid Book Language Given"))
|
||||||
|
else:
|
||||||
|
return _("Parameter not found"), 400
|
||||||
|
except Exception as ex:
|
||||||
|
log.debug_or_exception(ex)
|
||||||
|
return str(ex), 400
|
||||||
|
ub.session_commit()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
@admi.route("/ajax/user_table_settings", methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def update_table_settings():
|
||||||
|
current_user.view_settings['useredit'] = json.loads(request.data)
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
flag_modified(current_user, "view_settings")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
ub.session.commit()
|
||||||
|
except (InvalidRequestError, OperationalError):
|
||||||
|
log.error("Invalid request received: {}".format(request))
|
||||||
|
return "Invalid request", 400
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def check_valid_read_column(column):
|
||||||
|
if column != "0":
|
||||||
|
if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \
|
||||||
|
.filter(and_(db.Custom_Columns.datatype == 'bool', db.Custom_Columns.mark_for_delete == 0)).all():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_valid_restricted_column(column):
|
||||||
|
if column != "0":
|
||||||
|
if not calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.id == column) \
|
||||||
|
.filter(and_(db.Custom_Columns.datatype == 'text', db.Custom_Columns.mark_for_delete == 0)).all():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/viewconfig", methods=["POST"])
|
@admi.route("/admin/viewconfig", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -227,12 +503,23 @@ def update_view_configuration():
|
|||||||
if _config_string("config_title_regex"):
|
if _config_string("config_title_regex"):
|
||||||
calibre_db.update_title_sort(config)
|
calibre_db.update_title_sort(config)
|
||||||
|
|
||||||
|
if not check_valid_read_column(to_save.get("config_read_column", "0")):
|
||||||
|
flash(_(u"Invalid Read Column"), category="error")
|
||||||
|
log.debug("Invalid Read column")
|
||||||
|
return view_configuration()
|
||||||
_config_int("config_read_column")
|
_config_int("config_read_column")
|
||||||
|
|
||||||
|
if not check_valid_restricted_column(to_save.get("config_restricted_column", "0")):
|
||||||
|
flash(_(u"Invalid Restricted Column"), category="error")
|
||||||
|
log.debug("Invalid Restricted Column")
|
||||||
|
return view_configuration()
|
||||||
|
_config_int("config_restricted_column")
|
||||||
|
|
||||||
_config_int("config_theme")
|
_config_int("config_theme")
|
||||||
_config_int("config_random_books")
|
_config_int("config_random_books")
|
||||||
_config_int("config_books_per_page")
|
_config_int("config_books_per_page")
|
||||||
_config_int("config_authors_max")
|
_config_int("config_authors_max")
|
||||||
_config_int("config_restricted_column")
|
|
||||||
|
|
||||||
config.config_default_role = constants.selected_roles(to_save)
|
config.config_default_role = constants.selected_roles(to_save)
|
||||||
config.config_default_role &= ~constants.ROLE_ANONYMOUS
|
config.config_default_role &= ~constants.ROLE_ANONYMOUS
|
||||||
@@ -243,6 +530,7 @@ def update_view_configuration():
|
|||||||
|
|
||||||
config.save()
|
config.save()
|
||||||
flash(_(u"Calibre-Web configuration updated"), category="success")
|
flash(_(u"Calibre-Web configuration updated"), category="success")
|
||||||
|
log.debug("Calibre-Web configuration updated")
|
||||||
before_request()
|
before_request()
|
||||||
|
|
||||||
return view_configuration()
|
return view_configuration()
|
||||||
@@ -260,6 +548,16 @@ def load_dialogtexts(element_id):
|
|||||||
texts["main"] = _('Do you really want to delete this user?')
|
texts["main"] = _('Do you really want to delete this user?')
|
||||||
elif element_id == "delete_shelf":
|
elif element_id == "delete_shelf":
|
||||||
texts["main"] = _('Are you sure you want to delete this shelf?')
|
texts["main"] = _('Are you sure you want to delete this shelf?')
|
||||||
|
elif element_id == "select_locale":
|
||||||
|
texts["main"] = _('Are you sure you want to change locales of selected user(s)?')
|
||||||
|
elif element_id == "select_default_language":
|
||||||
|
texts["main"] = _('Are you sure you want to change visible book languages for selected user(s)?')
|
||||||
|
elif element_id == "role":
|
||||||
|
texts["main"] = _('Are you sure you want to change the selected role for the selected user(s)?')
|
||||||
|
elif element_id == "restrictions":
|
||||||
|
texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?')
|
||||||
|
elif element_id == "sidebar_view":
|
||||||
|
texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?')
|
||||||
return json.dumps(texts)
|
return json.dumps(texts)
|
||||||
|
|
||||||
|
|
||||||
@@ -346,7 +644,7 @@ def edit_restriction(res_type, user_id):
|
|||||||
elementlist = usr.list_allowed_tags()
|
elementlist = usr.list_allowed_tags()
|
||||||
elementlist[int(element['id'][1:])] = element['Element']
|
elementlist[int(element['id'][1:])] = element['Element']
|
||||||
usr.allowed_tags = ','.join(elementlist)
|
usr.allowed_tags = ','.join(elementlist)
|
||||||
ub.session_commit("Changed allowed tags of user {} to {}".format(usr.nickname, usr.allowed_tags))
|
ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.allowed_tags))
|
||||||
if res_type == 3: # CColumn per user
|
if res_type == 3: # CColumn per user
|
||||||
if isinstance(user_id, int):
|
if isinstance(user_id, int):
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
@@ -355,7 +653,7 @@ def edit_restriction(res_type, user_id):
|
|||||||
elementlist = usr.list_allowed_column_values()
|
elementlist = usr.list_allowed_column_values()
|
||||||
elementlist[int(element['id'][1:])] = element['Element']
|
elementlist[int(element['id'][1:])] = element['Element']
|
||||||
usr.allowed_column_value = ','.join(elementlist)
|
usr.allowed_column_value = ','.join(elementlist)
|
||||||
ub.session_commit("Changed allowed columns of user {} to {}".format(usr.nickname, usr.allowed_column_value))
|
ub.session_commit("Changed allowed columns of user {} to {}".format(usr.name, usr.allowed_column_value))
|
||||||
if element['id'].startswith('d'):
|
if element['id'].startswith('d'):
|
||||||
if res_type == 0: # Tags as template
|
if res_type == 0: # Tags as template
|
||||||
elementlist = config.list_denied_tags()
|
elementlist = config.list_denied_tags()
|
||||||
@@ -375,7 +673,7 @@ def edit_restriction(res_type, user_id):
|
|||||||
elementlist = usr.list_denied_tags()
|
elementlist = usr.list_denied_tags()
|
||||||
elementlist[int(element['id'][1:])] = element['Element']
|
elementlist[int(element['id'][1:])] = element['Element']
|
||||||
usr.denied_tags = ','.join(elementlist)
|
usr.denied_tags = ','.join(elementlist)
|
||||||
ub.session_commit("Changed denied tags of user {} to {}".format(usr.nickname, usr.denied_tags))
|
ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.denied_tags))
|
||||||
if res_type == 3: # CColumn per user
|
if res_type == 3: # CColumn per user
|
||||||
if isinstance(user_id, int):
|
if isinstance(user_id, int):
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
@@ -384,7 +682,7 @@ def edit_restriction(res_type, user_id):
|
|||||||
elementlist = usr.list_denied_column_values()
|
elementlist = usr.list_denied_column_values()
|
||||||
elementlist[int(element['id'][1:])] = element['Element']
|
elementlist[int(element['id'][1:])] = element['Element']
|
||||||
usr.denied_column_value = ','.join(elementlist)
|
usr.denied_column_value = ','.join(elementlist)
|
||||||
ub.session_commit("Changed denied columns of user {} to {}".format(usr.nickname, usr.denied_column_value))
|
ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, usr.denied_column_value))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -404,6 +702,26 @@ def restriction_deletion(element, list_func):
|
|||||||
return ','.join(elementlist)
|
return ','.join(elementlist)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_tags(user, action, tags_name, id_list):
|
||||||
|
if "tags" in tags_name:
|
||||||
|
tags = calibre_db.session.query(db.Tags).filter(db.Tags.id.in_(id_list)).all()
|
||||||
|
if not tags:
|
||||||
|
raise Exception(_("Tag not found"))
|
||||||
|
new_tags_list = [x.name for x in tags]
|
||||||
|
else:
|
||||||
|
tags = calibre_db.session.query(db.cc_classes[config.config_restricted_column])\
|
||||||
|
.filter(db.cc_classes[config.config_restricted_column].id.in_(id_list)).all()
|
||||||
|
new_tags_list = [x.value for x in tags]
|
||||||
|
saved_tags_list = user.__dict__[tags_name].split(",") if len(user.__dict__[tags_name]) else []
|
||||||
|
if action == "remove":
|
||||||
|
saved_tags_list = [x for x in saved_tags_list if x not in new_tags_list]
|
||||||
|
elif action == "add":
|
||||||
|
saved_tags_list.extend(x for x in new_tags_list if x not in saved_tags_list)
|
||||||
|
else:
|
||||||
|
raise Exception(_("Invalid Action"))
|
||||||
|
return ",".join(saved_tags_list)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/ajax/addrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
|
@admi.route("/ajax/addrestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
|
||||||
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@@ -431,10 +749,10 @@ def add_restriction(res_type, user_id):
|
|||||||
usr = current_user
|
usr = current_user
|
||||||
if 'submit_allow' in element:
|
if 'submit_allow' in element:
|
||||||
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
|
usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags)
|
||||||
ub.session_commit("Changed allowed tags of user {} to {}".format(usr.nickname, usr.list_allowed_tags))
|
ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.list_allowed_tags()))
|
||||||
elif 'submit_deny' in element:
|
elif 'submit_deny' in element:
|
||||||
usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
|
usr.denied_tags = restriction_addition(element, usr.list_denied_tags)
|
||||||
ub.session_commit("Changed denied tags of user {} to {}".format(usr.nickname, usr.list_denied_tags))
|
ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.list_denied_tags()))
|
||||||
if res_type == 3: # CustomC per user
|
if res_type == 3: # CustomC per user
|
||||||
if isinstance(user_id, int):
|
if isinstance(user_id, int):
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
@@ -442,12 +760,12 @@ def add_restriction(res_type, user_id):
|
|||||||
usr = current_user
|
usr = current_user
|
||||||
if 'submit_allow' in element:
|
if 'submit_allow' in element:
|
||||||
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
|
usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values)
|
||||||
ub.session_commit("Changed allowed columns of user {} to {}".format(usr.nickname,
|
ub.session_commit("Changed allowed columns of user {} to {}".format(usr.name,
|
||||||
usr.list_allowed_column_values))
|
usr.list_allowed_column_values()))
|
||||||
elif 'submit_deny' in element:
|
elif 'submit_deny' in element:
|
||||||
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
|
usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values)
|
||||||
ub.session_commit("Changed denied columns of user {} to {}".format(usr.nickname,
|
ub.session_commit("Changed denied columns of user {} to {}".format(usr.name,
|
||||||
usr.list_denied_column_values))
|
usr.list_denied_column_values()))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -478,10 +796,10 @@ def delete_restriction(res_type, user_id):
|
|||||||
usr = current_user
|
usr = current_user
|
||||||
if element['id'].startswith('a'):
|
if element['id'].startswith('a'):
|
||||||
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
|
usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags)
|
||||||
ub.session_commit("Deleted allowed tags of user {}: {}".format(usr.nickname, usr.list_allowed_tags))
|
ub.session_commit("Deleted allowed tags of user {}: {}".format(usr.name, usr.list_allowed_tags))
|
||||||
elif element['id'].startswith('d'):
|
elif element['id'].startswith('d'):
|
||||||
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
|
usr.denied_tags = restriction_deletion(element, usr.list_denied_tags)
|
||||||
ub.session_commit("Deleted denied tags of user {}: {}".format(usr.nickname, usr.list_allowed_tags))
|
ub.session_commit("Deleted denied tags of user {}: {}".format(usr.name, usr.list_allowed_tags))
|
||||||
elif res_type == 3: # Columns per user
|
elif res_type == 3: # Columns per user
|
||||||
if isinstance(user_id, int):
|
if isinstance(user_id, int):
|
||||||
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first()
|
||||||
@@ -489,12 +807,12 @@ def delete_restriction(res_type, user_id):
|
|||||||
usr = current_user
|
usr = current_user
|
||||||
if element['id'].startswith('a'):
|
if element['id'].startswith('a'):
|
||||||
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
|
usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values)
|
||||||
ub.session_commit("Deleted allowed columns of user {}: {}".format(usr.nickname,
|
ub.session_commit("Deleted allowed columns of user {}: {}".format(usr.name,
|
||||||
usr.list_allowed_column_values))
|
usr.list_allowed_column_values))
|
||||||
|
|
||||||
elif element['id'].startswith('d'):
|
elif element['id'].startswith('d'):
|
||||||
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
|
usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values)
|
||||||
ub.session_commit("Deleted denied columns of user {}: {}".format(usr.nickname,
|
ub.session_commit("Deleted denied columns of user {}: {}".format(usr.name,
|
||||||
usr.list_denied_column_values))
|
usr.list_denied_column_values))
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -600,7 +918,6 @@ def pathchooser():
|
|||||||
folders = []
|
folders = []
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
# locale = get_locale()
|
|
||||||
for f in folders:
|
for f in folders:
|
||||||
try:
|
try:
|
||||||
data = {"name": f, "fullpath": os.path.join(cwd, f)}
|
data = {"name": f, "fullpath": os.path.join(cwd, f)}
|
||||||
@@ -636,7 +953,7 @@ def pathchooser():
|
|||||||
"parentdir": parentdir,
|
"parentdir": parentdir,
|
||||||
"type": browse_for,
|
"type": browse_for,
|
||||||
"oldfile": oldfile,
|
"oldfile": oldfile,
|
||||||
"absolute": abs,
|
"absolute": absolute,
|
||||||
}
|
}
|
||||||
return json.dumps(context)
|
return json.dumps(context)
|
||||||
|
|
||||||
@@ -646,6 +963,7 @@ def pathchooser():
|
|||||||
def basic_configuration():
|
def basic_configuration():
|
||||||
logout_user()
|
logout_user()
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
log.debug("Basic Configuration send")
|
||||||
return _configuration_update_helper(configured=filepicker)
|
return _configuration_update_helper(configured=filepicker)
|
||||||
return _configuration_result(configured=filepicker)
|
return _configuration_result(configured=filepicker)
|
||||||
|
|
||||||
@@ -667,10 +985,13 @@ def _config_string(to_save, x):
|
|||||||
|
|
||||||
|
|
||||||
def _configuration_gdrive_helper(to_save):
|
def _configuration_gdrive_helper(to_save):
|
||||||
|
gdrive_error = None
|
||||||
|
gdrive_secrets = {}
|
||||||
|
|
||||||
if not os.path.isfile(gdriveutils.SETTINGS_YAML):
|
if not os.path.isfile(gdriveutils.SETTINGS_YAML):
|
||||||
config.config_use_google_drive = False
|
config.config_use_google_drive = False
|
||||||
|
|
||||||
gdrive_secrets = {}
|
if gdrive_support:
|
||||||
gdrive_error = gdriveutils.get_error_text(gdrive_secrets)
|
gdrive_error = gdriveutils.get_error_text(gdrive_secrets)
|
||||||
if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error:
|
if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error:
|
||||||
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
|
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
|
||||||
@@ -684,7 +1005,10 @@ def _configuration_gdrive_helper(to_save):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# always show google drive settings, but in case of error deny support
|
# always show google drive settings, but in case of error deny support
|
||||||
config.config_use_google_drive = (not gdrive_error) and ("config_use_google_drive" in to_save)
|
new_gdrive_value = (not gdrive_error) and ("config_use_google_drive" in to_save)
|
||||||
|
if config.config_use_google_drive and not new_gdrive_value:
|
||||||
|
config.config_google_drive_watch_changes_response = {}
|
||||||
|
config.config_use_google_drive = new_gdrive_value
|
||||||
if _config_string(to_save, "config_google_drive_folder"):
|
if _config_string(to_save, "config_google_drive_folder"):
|
||||||
gdriveutils.deleteDatabaseOnChange()
|
gdriveutils.deleteDatabaseOnChange()
|
||||||
return gdrive_error
|
return gdrive_error
|
||||||
@@ -745,7 +1069,7 @@ def _configuration_ldap_helper(to_save, gdrive_error):
|
|||||||
reboot_required |= _config_string(to_save, "config_ldap_cert_path")
|
reboot_required |= _config_string(to_save, "config_ldap_cert_path")
|
||||||
reboot_required |= _config_string(to_save, "config_ldap_key_path")
|
reboot_required |= _config_string(to_save, "config_ldap_key_path")
|
||||||
_config_string(to_save, "config_ldap_group_name")
|
_config_string(to_save, "config_ldap_group_name")
|
||||||
if "config_ldap_serv_password" in to_save and to_save["config_ldap_serv_password"] != "":
|
if to_save.get("config_ldap_serv_password", "") != "":
|
||||||
reboot_required |= 1
|
reboot_required |= 1
|
||||||
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8')
|
config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8')
|
||||||
config.save()
|
config.save()
|
||||||
@@ -783,7 +1107,7 @@ def _configuration_ldap_helper(to_save, gdrive_error):
|
|||||||
return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'),
|
return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'),
|
||||||
gdrive_error)
|
gdrive_error)
|
||||||
|
|
||||||
if to_save["ldap_import_user_filter"] == '0':
|
if to_save.get("ldap_import_user_filter") == '0':
|
||||||
config.config_ldap_member_user_object = ""
|
config.config_ldap_member_user_object = ""
|
||||||
else:
|
else:
|
||||||
if config.config_ldap_member_user_object.count("%s") != 1:
|
if config.config_ldap_member_user_object.count("%s") != 1:
|
||||||
@@ -901,15 +1225,16 @@ def _configuration_update_helper(configured):
|
|||||||
return _configuration_result(unrar_status, gdrive_error, configured)
|
return _configuration_result(unrar_status, gdrive_error, configured)
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
_configuration_result(_(u"Settings DB is not Writeable"), gdrive_error, configured)
|
log.error("Settings DB is not Writeable")
|
||||||
|
_configuration_result(_("Settings DB is not Writeable"), gdrive_error, configured)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
|
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
|
||||||
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
|
if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db):
|
||||||
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
|
gdriveutils.downloadFile(None, "metadata.db", metadata_db)
|
||||||
db_change = True
|
db_change = True
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
return _configuration_result('%s' % e, gdrive_error, configured)
|
return _configuration_result('%s' % ex, gdrive_error, configured)
|
||||||
|
|
||||||
if db_change:
|
if db_change:
|
||||||
if not calibre_db.setup_db(config, ub.app_DB_path):
|
if not calibre_db.setup_db(config, ub.app_DB_path):
|
||||||
@@ -932,16 +1257,17 @@ def _configuration_result(error_flash=None, gdrive_error=None, configured=True):
|
|||||||
gdrivefolders = []
|
gdrivefolders = []
|
||||||
if gdrive_error is None:
|
if gdrive_error is None:
|
||||||
gdrive_error = gdriveutils.get_error_text()
|
gdrive_error = gdriveutils.get_error_text()
|
||||||
if gdrive_error:
|
if gdrive_error and gdrive_support:
|
||||||
|
log.error(gdrive_error)
|
||||||
gdrive_error = _(gdrive_error)
|
gdrive_error = _(gdrive_error)
|
||||||
else:
|
else:
|
||||||
# if config.config_use_google_drive and\
|
|
||||||
if not gdrive_authenticate and gdrive_support:
|
if not gdrive_authenticate and gdrive_support:
|
||||||
gdrivefolders = gdriveutils.listRootFolders()
|
gdrivefolders = gdriveutils.listRootFolders()
|
||||||
|
|
||||||
show_back_button = current_user.is_authenticated
|
show_back_button = current_user.is_authenticated
|
||||||
show_login_button = config.db_configured and not current_user.is_authenticated
|
show_login_button = config.db_configured and not current_user.is_authenticated
|
||||||
if error_flash:
|
if error_flash:
|
||||||
|
log.error(error_flash)
|
||||||
config.load()
|
config.load()
|
||||||
flash(error_flash, category="error")
|
flash(error_flash, category="error")
|
||||||
show_login_button = False
|
show_login_button = False
|
||||||
@@ -961,7 +1287,6 @@ def _configuration_result(error_flash=None, gdrive_error=None, configured=True):
|
|||||||
|
|
||||||
def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||||
content.default_language = to_save["default_language"]
|
content.default_language = to_save["default_language"]
|
||||||
# content.mature_content = "Show_mature_content" in to_save
|
|
||||||
content.locale = to_save.get("locale", content.locale)
|
content.locale = to_save.get("locale", content.locale)
|
||||||
|
|
||||||
content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_'))
|
content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_'))
|
||||||
@@ -969,28 +1294,21 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
|||||||
content.sidebar_view |= constants.DETAIL_RANDOM
|
content.sidebar_view |= constants.DETAIL_RANDOM
|
||||||
|
|
||||||
content.role = constants.selected_roles(to_save)
|
content.role = constants.selected_roles(to_save)
|
||||||
|
|
||||||
if not to_save["nickname"] or not to_save["email"] or not to_save["password"]:
|
|
||||||
flash(_(u"Please fill out all fields!"), category="error")
|
|
||||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
|
||||||
registered_oauth=oauth_check, kobo_support=kobo_support,
|
|
||||||
title=_(u"Add new user"))
|
|
||||||
content.password = generate_password_hash(to_save["password"])
|
content.password = generate_password_hash(to_save["password"])
|
||||||
existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower()) \
|
try:
|
||||||
.first()
|
if not to_save["name"] or not to_save["email"] or not to_save["password"]:
|
||||||
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \
|
log.info("Missing entries on new user")
|
||||||
.first()
|
raise Exception(_(u"Please fill out all fields!"))
|
||||||
if not existing_user and not existing_email:
|
content.email = check_email(to_save["email"])
|
||||||
content.nickname = to_save["nickname"]
|
# Query User name, if not existing, change
|
||||||
if config.config_public_reg and not check_valid_domain(to_save["email"]):
|
content.name = check_username(to_save["name"])
|
||||||
flash(_(u"E-mail is not from valid domain"), category="error")
|
if to_save.get("kindle_mail"):
|
||||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
content.kindle_mail = valid_email(to_save["kindle_mail"])
|
||||||
registered_oauth=oauth_check, kobo_support=kobo_support,
|
if config.config_public_reg and not check_valid_domain(content.email):
|
||||||
title=_(u"Add new user"))
|
log.info("E-mail: {} for new user is not from valid domain".format(content.email))
|
||||||
else:
|
raise Exception(_(u"E-mail is not from valid domain"))
|
||||||
content.email = to_save["email"]
|
except Exception as ex:
|
||||||
else:
|
flash(str(ex), category="error")
|
||||||
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
|
|
||||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||||
@@ -1001,34 +1319,49 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
|||||||
content.denied_column_value = config.config_denied_column_value
|
content.denied_column_value = config.config_denied_column_value
|
||||||
ub.session.add(content)
|
ub.session.add(content)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
flash(_(u"User '%(user)s' created", user=content.nickname), category="success")
|
flash(_(u"User '%(user)s' created", user=content.name), category="success")
|
||||||
|
log.debug("User {} created".format(content.name))
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Found an existing account for this e-mail address or nickname."), category="error")
|
log.error("Found an existing account for {} or {}".format(content.name, content.email))
|
||||||
|
flash(_("Found an existing account for this e-mail address or name."), category="error")
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
|
|
||||||
|
def _delete_user(content):
|
||||||
|
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
||||||
|
ub.User.id != content.id).count():
|
||||||
|
if content.name != "Guest":
|
||||||
|
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
|
||||||
|
ub.session_commit()
|
||||||
|
log.info(u"User {} deleted".format(content.name))
|
||||||
|
return(_(u"User '%(nick)s' deleted", nick=content.name))
|
||||||
|
else:
|
||||||
|
log.warning(_(u"Can't delete Guest User"))
|
||||||
|
raise Exception(_(u"Can't delete Guest User"))
|
||||||
|
else:
|
||||||
|
log.warning(u"No admin user remaining, can't delete user")
|
||||||
|
raise Exception(_(u"No admin user remaining, can't delete user"))
|
||||||
|
|
||||||
|
|
||||||
def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
||||||
if "delete" in to_save:
|
if to_save.get("delete"):
|
||||||
if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
try:
|
||||||
ub.User.id != content.id).count():
|
flash(_delete_user(content), category="success")
|
||||||
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
|
except Exception as ex:
|
||||||
ub.session_commit()
|
log.error(ex)
|
||||||
flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success")
|
flash(str(ex), category="error")
|
||||||
return redirect(url_for('admin.admin'))
|
|
||||||
else:
|
|
||||||
flash(_(u"No admin user remaining, can't delete user", nick=content.nickname), category="error")
|
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
else:
|
else:
|
||||||
if not ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
if not ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN,
|
||||||
ub.User.id != content.id).count() and 'admin_role' not in to_save:
|
ub.User.id != content.id).count() and 'admin_role' not in to_save:
|
||||||
flash(_(u"No admin user remaining, can't remove admin role", nick=content.nickname), category="error")
|
log.warning("No admin user remaining, can't remove admin role from {}".format(content.name))
|
||||||
|
flash(_("No admin user remaining, can't remove admin role"), category="error")
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
|
if to_save.get("password"):
|
||||||
if "password" in to_save and to_save["password"]:
|
|
||||||
content.password = generate_password_hash(to_save["password"])
|
content.password = generate_password_hash(to_save["password"])
|
||||||
anonymous = content.is_anonymous
|
anonymous = content.is_anonymous
|
||||||
content.role = constants.selected_roles(to_save)
|
content.role = constants.selected_roles(to_save)
|
||||||
@@ -1046,22 +1379,28 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
|||||||
elif value not in val and content.check_visibility(value):
|
elif value not in val and content.check_visibility(value):
|
||||||
content.sidebar_view &= ~value
|
content.sidebar_view &= ~value
|
||||||
|
|
||||||
if "Show_detail_random" in to_save:
|
if to_save.get("Show_detail_random"):
|
||||||
content.sidebar_view |= constants.DETAIL_RANDOM
|
content.sidebar_view |= constants.DETAIL_RANDOM
|
||||||
else:
|
else:
|
||||||
content.sidebar_view &= ~constants.DETAIL_RANDOM
|
content.sidebar_view &= ~constants.DETAIL_RANDOM
|
||||||
|
|
||||||
if "default_language" in to_save:
|
if to_save.get("default_language"):
|
||||||
content.default_language = to_save["default_language"]
|
content.default_language = to_save["default_language"]
|
||||||
if "locale" in to_save and to_save["locale"]:
|
if to_save.get("locale"):
|
||||||
content.locale = to_save["locale"]
|
content.locale = to_save["locale"]
|
||||||
if to_save["email"] and to_save["email"] != content.email:
|
try:
|
||||||
existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \
|
if to_save.get("email", content.email) != content.email:
|
||||||
.first()
|
content.email = check_email(to_save["email"])
|
||||||
if not existing_email:
|
# Query User name, if not existing, change
|
||||||
content.email = to_save["email"]
|
if to_save.get("name", content.name) != content.name:
|
||||||
else:
|
if to_save.get("name") == "Guest":
|
||||||
flash(_(u"Found an existing account for this e-mail address."), category="error")
|
raise Exception(_("Guest Name can't be changed"))
|
||||||
|
content.name = check_username(to_save["name"])
|
||||||
|
if to_save.get("kindle_mail") != content.kindle_mail:
|
||||||
|
content.kindle_mail = valid_email(to_save["kindle_mail"]) if to_save["kindle_mail"] else ""
|
||||||
|
except Exception as ex:
|
||||||
|
log.error(ex)
|
||||||
|
flash(str(ex), category="error")
|
||||||
return render_title_template("user_edit.html",
|
return render_title_template("user_edit.html",
|
||||||
translations=translations,
|
translations=translations,
|
||||||
languages=languages,
|
languages=languages,
|
||||||
@@ -1070,34 +1409,20 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
|||||||
new_user=0,
|
new_user=0,
|
||||||
content=content,
|
content=content,
|
||||||
registered_oauth=oauth_check,
|
registered_oauth=oauth_check,
|
||||||
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
title=_(u"Edit User %(nick)s", nick=content.name),
|
||||||
if "nickname" in to_save and to_save["nickname"] != content.nickname:
|
|
||||||
# Query User nickname, if not existing, change
|
|
||||||
if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar():
|
|
||||||
content.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,
|
|
||||||
mail_configured=config.get_mail_server_configured(),
|
|
||||||
new_user=0, content=content,
|
|
||||||
registered_oauth=oauth_check,
|
|
||||||
kobo_support=kobo_support,
|
|
||||||
title=_(u"Edit User %(nick)s", nick=content.nickname),
|
|
||||||
page="edituser")
|
page="edituser")
|
||||||
|
|
||||||
if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail:
|
|
||||||
content.kindle_mail = to_save["kindle_mail"]
|
|
||||||
try:
|
try:
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success")
|
flash(_(u"User '%(nick)s' updated", nick=content.name), category="success")
|
||||||
except IntegrityError:
|
except IntegrityError as ex:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"An unknown error occured."), category="error")
|
log.error("An unknown error occurred while changing user: {}".format(str(ex)))
|
||||||
|
flash(_(u"An unknown error occurred. Please try again later."), category="error")
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/user/new", methods=["GET", "POST"])
|
@admi.route("/admin/user/new", methods=["GET", "POST"])
|
||||||
@@ -1125,7 +1450,7 @@ def new_user():
|
|||||||
def edit_mailsettings():
|
def edit_mailsettings():
|
||||||
content = config.get_mail_settings()
|
content = config.get_mail_settings()
|
||||||
return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"),
|
return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"),
|
||||||
page="mailset")
|
page="mailset", feature_support=feature_support)
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/mailsettings", methods=["POST"])
|
@admi.route("/admin/mailsettings", methods=["POST"])
|
||||||
@@ -1133,8 +1458,23 @@ def edit_mailsettings():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def update_mailsettings():
|
def update_mailsettings():
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
# log.debug("update_mailsettings %r", to_save)
|
_config_int(to_save, "mail_server_type")
|
||||||
|
if to_save.get("invalidate"):
|
||||||
|
config.mail_gmail_token = {}
|
||||||
|
try:
|
||||||
|
flag_modified(config, "mail_gmail_token")
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
elif to_save.get("gmail"):
|
||||||
|
try:
|
||||||
|
config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token)
|
||||||
|
flash(_(u"Gmail Account Verification Successful"), category="success")
|
||||||
|
except Exception as ex:
|
||||||
|
flash(str(ex), category="error")
|
||||||
|
log.error(ex)
|
||||||
|
return edit_mailsettings()
|
||||||
|
|
||||||
|
else:
|
||||||
_config_string(to_save, "mail_server")
|
_config_string(to_save, "mail_server")
|
||||||
_config_int(to_save, "mail_port")
|
_config_int(to_save, "mail_port")
|
||||||
_config_int(to_save, "mail_use_ssl")
|
_config_int(to_save, "mail_use_ssl")
|
||||||
@@ -1146,15 +1486,16 @@ def update_mailsettings():
|
|||||||
config.save()
|
config.save()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
return edit_mailsettings()
|
return edit_mailsettings()
|
||||||
|
|
||||||
if to_save.get("test"):
|
if to_save.get("test"):
|
||||||
if current_user.email:
|
if current_user.email:
|
||||||
result = send_test_mail(current_user.email, current_user.nickname)
|
result = send_test_mail(current_user.email, current_user.name)
|
||||||
if result is None:
|
if result is None:
|
||||||
flash(_(u"Test e-mail successfully send to %(kindlemail)s", kindlemail=current_user.email),
|
flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result",
|
||||||
category="success")
|
email=current_user.email), category="info")
|
||||||
else:
|
else:
|
||||||
flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error")
|
flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error")
|
||||||
else:
|
else:
|
||||||
@@ -1170,7 +1511,7 @@ def update_mailsettings():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def edit_user(user_id):
|
def edit_user(user_id):
|
||||||
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User
|
||||||
if not content or (not config.config_anonbrowse and content.nickname == "Guest"):
|
if not content or (not config.config_anonbrowse and content.name == "Guest"):
|
||||||
flash(_(u"User not found"), category="error")
|
flash(_(u"User not found"), category="error")
|
||||||
return redirect(url_for('admin.admin'))
|
return redirect(url_for('admin.admin'))
|
||||||
languages = calibre_db.speaking_language()
|
languages = calibre_db.speaking_language()
|
||||||
@@ -1178,7 +1519,9 @@ def edit_user(user_id):
|
|||||||
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
to_save = request.form.to_dict()
|
to_save = request.form.to_dict()
|
||||||
_handle_edit_user(to_save, content, languages, translations, kobo_support)
|
resp = _handle_edit_user(to_save, content, languages, translations, kobo_support)
|
||||||
|
if resp:
|
||||||
|
return resp
|
||||||
return render_title_template("user_edit.html",
|
return render_title_template("user_edit.html",
|
||||||
translations=translations,
|
translations=translations,
|
||||||
languages=languages,
|
languages=languages,
|
||||||
@@ -1187,7 +1530,8 @@ def edit_user(user_id):
|
|||||||
registered_oauth=oauth_check,
|
registered_oauth=oauth_check,
|
||||||
mail_configured=config.get_mail_server_configured(),
|
mail_configured=config.get_mail_server_configured(),
|
||||||
kobo_support=kobo_support,
|
kobo_support=kobo_support,
|
||||||
title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser")
|
title=_(u"Edit User %(nick)s", nick=content.name),
|
||||||
|
page="edituser")
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/admin/resetpassword/<int:user_id>")
|
@admi.route("/admin/resetpassword/<int:user_id>")
|
||||||
@@ -1264,8 +1608,11 @@ def download_debug():
|
|||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
def get_update_status():
|
def get_update_status():
|
||||||
|
if feature_support['updater']:
|
||||||
log.info(u"Update status requested")
|
log.info(u"Update status requested")
|
||||||
return updater_thread.get_available_updates(request.method, locale=get_locale())
|
return updater_thread.get_available_updates(request.method, locale=get_locale())
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
@admi.route("/get_updater_status", methods=['GET', 'POST'])
|
@admi.route("/get_updater_status", methods=['GET', 'POST'])
|
||||||
@@ -1273,6 +1620,7 @@ def get_update_status():
|
|||||||
@admin_required
|
@admin_required
|
||||||
def get_updater_status():
|
def get_updater_status():
|
||||||
status = {}
|
status = {}
|
||||||
|
if feature_support['updater']:
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
commit = request.form.to_dict()
|
commit = request.form.to_dict()
|
||||||
if "start" in commit and commit['start'] == 'True':
|
if "start" in commit and commit['start'] == 'True':
|
||||||
@@ -1302,6 +1650,55 @@ def get_updater_status():
|
|||||||
except Exception:
|
except Exception:
|
||||||
status['status'] = 11
|
status['status'] = 11
|
||||||
return json.dumps(status)
|
return json.dumps(status)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def ldap_import_create_user(user, user_data):
|
||||||
|
user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object)
|
||||||
|
|
||||||
|
username = user_data[user_login_field][0].decode('utf-8')
|
||||||
|
# check for duplicate username
|
||||||
|
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first():
|
||||||
|
# if ub.session.query(ub.User).filter(ub.User.name == username).first():
|
||||||
|
log.warning("LDAP User %s Already in Database", user_data)
|
||||||
|
return 0, None
|
||||||
|
|
||||||
|
kindlemail = ''
|
||||||
|
if 'mail' in user_data:
|
||||||
|
useremail = user_data['mail'][0].decode('utf-8')
|
||||||
|
if len(user_data['mail']) > 1:
|
||||||
|
kindlemail = user_data['mail'][1].decode('utf-8')
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.debug('No Mail Field Found in LDAP Response')
|
||||||
|
useremail = username + '@email.com'
|
||||||
|
|
||||||
|
try:
|
||||||
|
# check for duplicate email
|
||||||
|
useremail = check_email(useremail)
|
||||||
|
except Exception as ex:
|
||||||
|
log.warning("LDAP Email Error: {}, {}".format(user_data, ex))
|
||||||
|
return 0, None
|
||||||
|
content = ub.User()
|
||||||
|
content.name = username
|
||||||
|
content.password = '' # dummy password which will be replaced by ldap one
|
||||||
|
content.email = useremail
|
||||||
|
content.kindle_mail = kindlemail
|
||||||
|
content.role = config.config_default_role
|
||||||
|
content.sidebar_view = config.config_default_show
|
||||||
|
content.allowed_tags = config.config_allowed_tags
|
||||||
|
content.denied_tags = config.config_denied_tags
|
||||||
|
content.allowed_column_value = config.config_allowed_column_value
|
||||||
|
content.denied_column_value = config.config_denied_column_value
|
||||||
|
ub.session.add(content)
|
||||||
|
try:
|
||||||
|
ub.session.commit()
|
||||||
|
return 1, None # increase no of users
|
||||||
|
except Exception as ex:
|
||||||
|
log.warning("Failed to create LDAP user: %s - %s", user, ex)
|
||||||
|
ub.session.rollback()
|
||||||
|
message = _(u'Failed to Create at Least One LDAP User')
|
||||||
|
return 0, message
|
||||||
|
|
||||||
|
|
||||||
@admi.route('/import_ldap_users')
|
@admi.route('/import_ldap_users')
|
||||||
@@ -1331,59 +1728,23 @@ def import_ldap_users():
|
|||||||
query_filter = config.config_ldap_user_object
|
query_filter = config.config_ldap_user_object
|
||||||
try:
|
try:
|
||||||
user_identifier = extract_user_identifier(user, query_filter)
|
user_identifier = extract_user_identifier(user, query_filter)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.warning(e)
|
log.warning(ex)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
user_identifier = user
|
user_identifier = user
|
||||||
query_filter = None
|
query_filter = None
|
||||||
try:
|
try:
|
||||||
user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter)
|
user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter)
|
||||||
except AttributeError as e:
|
except AttributeError as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
continue
|
continue
|
||||||
if user_data:
|
if user_data:
|
||||||
user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object)
|
user_count, message = ldap_import_create_user(user, user_data)
|
||||||
|
if message:
|
||||||
username = user_data[user_login_field][0].decode('utf-8')
|
showtext['text'] = message
|
||||||
# check for duplicate username
|
|
||||||
if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first():
|
|
||||||
# if ub.session.query(ub.User).filter(ub.User.nickname == username).first():
|
|
||||||
log.warning("LDAP User %s Already in Database", user_data)
|
|
||||||
continue
|
|
||||||
|
|
||||||
kindlemail = ''
|
|
||||||
if 'mail' in user_data:
|
|
||||||
useremail = user_data['mail'][0].decode('utf-8')
|
|
||||||
if len(user_data['mail']) > 1:
|
|
||||||
kindlemail = user_data['mail'][1].decode('utf-8')
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.debug('No Mail Field Found in LDAP Response')
|
imported += user_count
|
||||||
useremail = username + '@email.com'
|
|
||||||
# check for duplicate email
|
|
||||||
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first():
|
|
||||||
log.warning("LDAP Email %s Already in Database", user_data)
|
|
||||||
continue
|
|
||||||
content = ub.User()
|
|
||||||
content.nickname = username
|
|
||||||
content.password = '' # dummy password which will be replaced by ldap one
|
|
||||||
content.email = useremail
|
|
||||||
content.kindle_mail = kindlemail
|
|
||||||
content.role = config.config_default_role
|
|
||||||
content.sidebar_view = config.config_default_show
|
|
||||||
content.allowed_tags = config.config_allowed_tags
|
|
||||||
content.denied_tags = config.config_denied_tags
|
|
||||||
content.allowed_column_value = config.config_allowed_column_value
|
|
||||||
content.denied_column_value = config.config_denied_column_value
|
|
||||||
ub.session.add(content)
|
|
||||||
try:
|
|
||||||
ub.session.commit()
|
|
||||||
imported += 1
|
|
||||||
except Exception as e:
|
|
||||||
log.warning("Failed to create LDAP user: %s - %s", user, e)
|
|
||||||
ub.session.rollback()
|
|
||||||
showtext['text'] = _(u'Failed to Create at Least One LDAP User')
|
|
||||||
else:
|
else:
|
||||||
log.warning("LDAP User: %s Not Found", user)
|
log.warning("LDAP User: %s Not Found", user)
|
||||||
showtext['text'] = _(u'At Least One LDAP User Not Found in Database')
|
showtext['text'] = _(u'At Least One LDAP User Not Found in Database')
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ def init_cache_busting(app):
|
|||||||
# compute version component
|
# compute version component
|
||||||
rooted_filename = os.path.join(dirpath, filename)
|
rooted_filename = os.path.join(dirpath, filename)
|
||||||
with open(rooted_filename, 'rb') as f:
|
with open(rooted_filename, 'rb') as f:
|
||||||
file_hash = hashlib.md5(f.read()).hexdigest()[:7]
|
file_hash = hashlib.md5(f.read()).hexdigest()[:7] # nosec
|
||||||
|
|
||||||
# save version to tables
|
# save version to tables
|
||||||
file_path = rooted_filename.replace(static_folder, "")
|
file_path = rooted_filename.replace(static_folder, "")
|
||||||
@@ -64,6 +64,7 @@ def init_cache_busting(app):
|
|||||||
return filename.split("?", 1)[0]
|
return filename.split("?", 1)[0]
|
||||||
|
|
||||||
@app.url_defaults
|
@app.url_defaults
|
||||||
|
# pylint: disable=unused-variable
|
||||||
def reverse_to_cache_busted_url(endpoint, values):
|
def reverse_to_cache_busted_url(endpoint, values):
|
||||||
"""
|
"""
|
||||||
Make `url_for` produce busted filenames when using the 'static' endpoint.
|
Make `url_for` produce busted filenames when using the 'static' endpoint.
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ if args.c:
|
|||||||
if os.path.isfile(args.c):
|
if os.path.isfile(args.c):
|
||||||
certfilepath = args.c
|
certfilepath = args.c
|
||||||
else:
|
else:
|
||||||
print("Certfilepath is invalid. Exiting...")
|
print("Certfile path is invalid. Exiting...")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.c == "":
|
if args.c == "":
|
||||||
@@ -81,7 +81,7 @@ if args.k:
|
|||||||
if os.path.isfile(args.k):
|
if os.path.isfile(args.k):
|
||||||
keyfilepath = args.k
|
keyfilepath = args.k
|
||||||
else:
|
else:
|
||||||
print("Keyfilepath is invalid. Exiting...")
|
print("Keyfile path is invalid. Exiting...")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if (args.k and not args.c) or (not args.k and args.c):
|
if (args.k and not args.c) or (not args.k and args.c):
|
||||||
@@ -91,29 +91,29 @@ if (args.k and not args.c) or (not args.k and args.c):
|
|||||||
if args.k == "":
|
if args.k == "":
|
||||||
keyfilepath = ""
|
keyfilepath = ""
|
||||||
|
|
||||||
# handle and check ipadress argument
|
# handle and check ip address argument
|
||||||
ipadress = args.i or None
|
ip_address = args.i or None
|
||||||
if ipadress:
|
if ip_address:
|
||||||
try:
|
try:
|
||||||
# try to parse the given ip address with socket
|
# try to parse the given ip address with socket
|
||||||
if hasattr(socket, 'inet_pton'):
|
if hasattr(socket, 'inet_pton'):
|
||||||
if ':' in ipadress:
|
if ':' in ip_address:
|
||||||
socket.inet_pton(socket.AF_INET6, ipadress)
|
socket.inet_pton(socket.AF_INET6, ip_address)
|
||||||
else:
|
else:
|
||||||
socket.inet_pton(socket.AF_INET, ipadress)
|
socket.inet_pton(socket.AF_INET, ip_address)
|
||||||
else:
|
else:
|
||||||
# on windows python < 3.4, inet_pton is not available
|
# on windows python < 3.4, inet_pton is not available
|
||||||
# inet_atom only handles IPv4 addresses
|
# inet_atom only handles IPv4 addresses
|
||||||
socket.inet_aton(ipadress)
|
socket.inet_aton(ip_address)
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
print(ipadress, ':', err)
|
print(ip_address, ':', err)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# handle and check user password argument
|
# handle and check user password argument
|
||||||
user_credentials = args.s or None
|
user_credentials = args.s or None
|
||||||
if user_credentials and ":" not in user_credentials:
|
if user_credentials and ":" not in user_credentials:
|
||||||
print("No valid username:password format")
|
print("No valid 'username:password' format")
|
||||||
sys.exit(3)
|
sys.exit(3)
|
||||||
|
|
||||||
# Handles enableing of filepicker
|
# Handles enabling of filepicker
|
||||||
filepicker = args.f or None
|
filepicker = args.f or None
|
||||||
|
|||||||
@@ -74,19 +74,8 @@ 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):
|
||||||
def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
cover_data = None
|
||||||
cover_data = extension = None
|
|
||||||
if use_comic_meta:
|
|
||||||
archive = ComicArchive(tmp_file_name, rar_exe_path=rarExecutable)
|
|
||||||
for index, name in enumerate(archive.getPageNameList()):
|
|
||||||
ext = os.path.splitext(name)
|
|
||||||
if len(ext) > 1:
|
|
||||||
extension = ext[1].lower()
|
|
||||||
if extension in COVER_EXTENSIONS:
|
|
||||||
cover_data = archive.getPage(index)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if original_file_extension.upper() == '.CBZ':
|
if original_file_extension.upper() == '.CBZ':
|
||||||
cf = zipfile.ZipFile(tmp_file_name)
|
cf = zipfile.ZipFile(tmp_file_name)
|
||||||
for name in cf.namelist():
|
for name in cf.namelist():
|
||||||
@@ -116,8 +105,24 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
|||||||
if extension in COVER_EXTENSIONS:
|
if extension in COVER_EXTENSIONS:
|
||||||
cover_data = cf.read(name)
|
cover_data = cf.read(name)
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug('Rarfile failed with error: %s', e)
|
log.debug('Rarfile failed with error: %s', ex)
|
||||||
|
return cover_data
|
||||||
|
|
||||||
|
|
||||||
|
def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||||
|
cover_data = extension = None
|
||||||
|
if use_comic_meta:
|
||||||
|
archive = ComicArchive(tmp_file_name, rar_exe_path=rarExecutable)
|
||||||
|
for index, name in enumerate(archive.getPageNameList()):
|
||||||
|
ext = os.path.splitext(name)
|
||||||
|
if len(ext) > 1:
|
||||||
|
extension = ext[1].lower()
|
||||||
|
if extension in COVER_EXTENSIONS:
|
||||||
|
cover_data = archive.getPage(index)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
cover_data = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
|
||||||
return _cover_processing(tmp_file_name, cover_data, extension)
|
return _cover_processing(tmp_file_name, cover_data, extension)
|
||||||
|
|
||||||
|
|
||||||
@@ -142,13 +147,15 @@ 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="",
|
||||||
series=loadedMetadata.series or "",
|
series=loadedMetadata.series or "",
|
||||||
series_id=loadedMetadata.issue or "",
|
series_id=loadedMetadata.issue or "",
|
||||||
languages=loadedMetadata.language)
|
languages=loadedMetadata.language,
|
||||||
|
publisher="")
|
||||||
|
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
@@ -160,4 +167,5 @@ def get_comic_info(tmp_file_path, original_file_name, original_file_extension, r
|
|||||||
tags="",
|
tags="",
|
||||||
series="",
|
series="",
|
||||||
series_id="",
|
series_id="",
|
||||||
languages="")
|
languages="",
|
||||||
|
publisher="")
|
||||||
|
|||||||
@@ -20,12 +20,18 @@
|
|||||||
from __future__ import division, print_function, unicode_literals
|
from __future__ import division, print_function, unicode_literals
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
from sqlalchemy import Column, String, Integer, SmallInteger, Boolean, BLOB, JSON
|
||||||
from sqlalchemy.exc import OperationalError
|
from sqlalchemy.exc import OperationalError
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.sql.expression import text
|
||||||
|
try:
|
||||||
|
# Compatibility with sqlalchemy 2.0
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
except ImportError:
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
from . import constants, cli, logger, ub
|
from . import constants, cli, logger
|
||||||
|
|
||||||
|
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
@@ -35,7 +41,7 @@ class _Flask_Settings(_Base):
|
|||||||
__tablename__ = 'flask_settings'
|
__tablename__ = 'flask_settings'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
flask_session_key = Column(BLOB, default="")
|
flask_session_key = Column(BLOB, default=b"")
|
||||||
|
|
||||||
def __init__(self, key):
|
def __init__(self, key):
|
||||||
self.flask_session_key = key
|
self.flask_session_key = key
|
||||||
@@ -54,6 +60,8 @@ class _Settings(_Base):
|
|||||||
mail_password = Column(String, default='mypassword')
|
mail_password = Column(String, default='mypassword')
|
||||||
mail_from = Column(String, default='automailer <mail@example.com>')
|
mail_from = Column(String, default='automailer <mail@example.com>')
|
||||||
mail_size = Column(Integer, default=25*1024*1024)
|
mail_size = Column(Integer, default=25*1024*1024)
|
||||||
|
mail_server_type = Column(SmallInteger, default=0)
|
||||||
|
mail_gmail_token = Column(JSON, default={})
|
||||||
|
|
||||||
config_calibre_dir = Column(String)
|
config_calibre_dir = Column(String)
|
||||||
config_port = Column(Integer, default=constants.DEFAULT_PORT)
|
config_port = Column(Integer, default=constants.DEFAULT_PORT)
|
||||||
@@ -66,7 +74,7 @@ class _Settings(_Base):
|
|||||||
config_random_books = Column(Integer, default=4)
|
config_random_books = Column(Integer, default=4)
|
||||||
config_authors_max = Column(Integer, default=0)
|
config_authors_max = Column(Integer, default=0)
|
||||||
config_read_column = Column(Integer, default=0)
|
config_read_column = Column(Integer, default=0)
|
||||||
config_title_regex = Column(String, default=u'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
|
config_title_regex = Column(String, default=r'^(A|The|An|Der|Die|Das|Den|Ein|Eine|Einen|Dem|Des|Einem|Eines)\s+')
|
||||||
config_mature_content_tags = Column(String, default='')
|
config_mature_content_tags = Column(String, default='')
|
||||||
config_theme = Column(Integer, default=0)
|
config_theme = Column(Integer, default=0)
|
||||||
|
|
||||||
@@ -146,15 +154,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,8 +190,9 @@ class _ConfigSQL(object):
|
|||||||
return None
|
return None
|
||||||
return self.config_keyfile
|
return self.config_keyfile
|
||||||
|
|
||||||
def get_config_ipaddress(self):
|
@staticmethod
|
||||||
return cli.ipadress or ""
|
def get_config_ipaddress():
|
||||||
|
return cli.ip_address or ""
|
||||||
|
|
||||||
def _has_role(self, role_flag):
|
def _has_role(self, role_flag):
|
||||||
return constants.has_flag(self.config_default_role, role_flag)
|
return constants.has_flag(self.config_default_role, role_flag)
|
||||||
@@ -240,18 +250,18 @@ class _ConfigSQL(object):
|
|||||||
return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')}
|
return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')}
|
||||||
|
|
||||||
def get_mail_server_configured(self):
|
def get_mail_server_configured(self):
|
||||||
return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER)
|
return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0)
|
||||||
|
or (self.mail_gmail_token != {} and self.mail_server_type == 1))
|
||||||
|
|
||||||
|
|
||||||
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
|
def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None):
|
||||||
'''Possibly updates a field of this object.
|
"""Possibly updates a field of this object.
|
||||||
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
|
The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor.
|
||||||
|
|
||||||
:returns: `True` if the field has changed value
|
:returns: `True` if the field has changed value
|
||||||
'''
|
"""
|
||||||
new_value = dictionary.get(field, default)
|
new_value = dictionary.get(field, default)
|
||||||
if new_value is None:
|
if new_value is None:
|
||||||
# log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if field not in self.__dict__:
|
if field not in self.__dict__:
|
||||||
@@ -268,7 +278,6 @@ class _ConfigSQL(object):
|
|||||||
if current_value == new_value:
|
if current_value == new_value:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# log.debug("_ConfigSQL set_from_dictionary '%s' = %r (was %r)", field, new_value, current_value)
|
|
||||||
setattr(self, field, new_value)
|
setattr(self, field, new_value)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -299,6 +308,10 @@ 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(',')]
|
||||||
|
if os.environ.get('FLASK_DEBUG'):
|
||||||
|
logfile = logger.setup(logger.LOG_TO_STDOUT, logger.logging.DEBUG)
|
||||||
|
else:
|
||||||
|
# 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)
|
||||||
@@ -345,7 +358,7 @@ def _migrate_table(session, orm_class):
|
|||||||
if column_name[0] != '_':
|
if column_name[0] != '_':
|
||||||
try:
|
try:
|
||||||
session.query(column).first()
|
session.query(column).first()
|
||||||
except exc.OperationalError as err:
|
except OperationalError as err:
|
||||||
log.debug("%s: %s", column_name, err.args[0])
|
log.debug("%s: %s", column_name, err.args[0])
|
||||||
if column.default is not None:
|
if column.default is not None:
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
@@ -355,16 +368,23 @@ def _migrate_table(session, orm_class):
|
|||||||
column_default = ""
|
column_default = ""
|
||||||
else:
|
else:
|
||||||
if isinstance(column.default.arg, bool):
|
if isinstance(column.default.arg, bool):
|
||||||
column_default = ("DEFAULT %r" % int(column.default.arg))
|
column_default = "DEFAULT {}".format(int(column.default.arg))
|
||||||
else:
|
else:
|
||||||
column_default = ("DEFAULT %r" % column.default.arg)
|
column_default = "DEFAULT `{}`".format(column.default.arg)
|
||||||
alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
|
if isinstance(column.type, JSON):
|
||||||
|
column_type = "JSON"
|
||||||
|
else:
|
||||||
|
column_type = column.type
|
||||||
|
alter_table = text("ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__,
|
||||||
column_name,
|
column_name,
|
||||||
column.type,
|
column_type,
|
||||||
column_default)
|
column_default))
|
||||||
log.debug(alter_table)
|
log.debug(alter_table)
|
||||||
session.execute(alter_table)
|
session.execute(alter_table)
|
||||||
changed = True
|
changed = True
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
log.error("Database corrupt column: {}".format(column_name))
|
||||||
|
log.debug(e)
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
try:
|
try:
|
||||||
@@ -423,12 +443,12 @@ def load_configuration(session):
|
|||||||
session.commit()
|
session.commit()
|
||||||
conf = _ConfigSQL(session)
|
conf = _ConfigSQL(session)
|
||||||
# Migrate from global restrictions to user based restrictions
|
# Migrate from global restrictions to user based restrictions
|
||||||
if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
|
#if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "":
|
||||||
conf.config_denied_tags = conf.config_mature_content_tags
|
# conf.config_denied_tags = conf.config_mature_content_tags
|
||||||
conf.save()
|
# conf.save()
|
||||||
session.query(ub.User).filter(ub.User.mature_content != True). \
|
# session.query(ub.User).filter(ub.User.mature_content != True). \
|
||||||
update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
|
# update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False)
|
||||||
session.commit()
|
# session.commit()
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
def get_flask_session_key(session):
|
def get_flask_session_key(session):
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ import sys
|
|||||||
import os
|
import os
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
HOME_CONFIG = False
|
# if installed via pip this variable is set to true (empty file with name .HOMEDIR present)
|
||||||
|
HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR'))
|
||||||
|
|
||||||
|
#In executables updater is not available, so variable is set to False there
|
||||||
|
UPDATER_AVAILABLE = True
|
||||||
|
|
||||||
# Base dir is parent of current file, necessary if called from different folder
|
# Base dir is parent of current file, necessary if called from different folder
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
@@ -84,6 +88,26 @@ SIDEBAR_ARCHIVED = 1 << 15
|
|||||||
SIDEBAR_DOWNLOAD = 1 << 16
|
SIDEBAR_DOWNLOAD = 1 << 16
|
||||||
SIDEBAR_LIST = 1 << 17
|
SIDEBAR_LIST = 1 << 17
|
||||||
|
|
||||||
|
sidebar_settings = {
|
||||||
|
"detail_random": DETAIL_RANDOM,
|
||||||
|
"sidebar_language": SIDEBAR_LANGUAGE,
|
||||||
|
"sidebar_series": SIDEBAR_SERIES,
|
||||||
|
"sidebar_category": SIDEBAR_CATEGORY,
|
||||||
|
"sidebar_random": SIDEBAR_RANDOM,
|
||||||
|
"sidebar_author": SIDEBAR_AUTHOR,
|
||||||
|
"sidebar_best_rated": SIDEBAR_BEST_RATED,
|
||||||
|
"sidebar_read_and_unread": SIDEBAR_READ_AND_UNREAD,
|
||||||
|
"sidebar_recent": SIDEBAR_RECENT,
|
||||||
|
"sidebar_sorted": SIDEBAR_SORTED,
|
||||||
|
"sidebar_publisher": SIDEBAR_PUBLISHER,
|
||||||
|
"sidebar_rating": SIDEBAR_RATING,
|
||||||
|
"sidebar_format": SIDEBAR_FORMAT,
|
||||||
|
"sidebar_archived": SIDEBAR_ARCHIVED,
|
||||||
|
"sidebar_download": SIDEBAR_DOWNLOAD,
|
||||||
|
"sidebar_list": SIDEBAR_LIST,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
|
ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS
|
||||||
ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1
|
ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1
|
||||||
|
|
||||||
@@ -102,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
|
||||||
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:
|
||||||
@@ -128,9 +152,9 @@ def selected_roles(dictionary):
|
|||||||
|
|
||||||
# :rtype: BookMeta
|
# :rtype: BookMeta
|
||||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||||
'series_id, languages')
|
'series_id, languages, publisher')
|
||||||
|
|
||||||
STABLE_VERSION = {'version': '0.6.10'}
|
STABLE_VERSION = {'version': '0.6.12'}
|
||||||
|
|
||||||
NIGHTLY_VERSION = {}
|
NIGHTLY_VERSION = {}
|
||||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||||
|
|||||||
@@ -30,7 +30,13 @@ from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
|
|||||||
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
|
from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float
|
||||||
from sqlalchemy.orm import relationship, sessionmaker, scoped_session
|
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 DeclarativeMeta
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
try:
|
||||||
|
# Compatibility with sqlalchemy 2.0
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
except ImportError:
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
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 sqlalchemy.ext.associationproxy import association_proxy
|
||||||
@@ -38,6 +44,7 @@ from flask_login import current_user
|
|||||||
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 _
|
||||||
|
from flask import flash
|
||||||
|
|
||||||
from . import logger, ub, isoLanguages
|
from . import logger, ub, isoLanguages
|
||||||
from .pagination import Pagination
|
from .pagination import Pagination
|
||||||
@@ -50,7 +57,9 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
use_unidecode = False
|
use_unidecode = False
|
||||||
|
|
||||||
cc_exceptions = ['datetime', 'comments', 'composite', 'series']
|
log = logger.create()
|
||||||
|
|
||||||
|
cc_exceptions = ['composite', 'series']
|
||||||
cc_classes = {}
|
cc_classes = {}
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
@@ -113,6 +122,8 @@ class Identifiers(Base):
|
|||||||
return u"Douban"
|
return u"Douban"
|
||||||
elif format_type == "goodreads":
|
elif format_type == "goodreads":
|
||||||
return u"Goodreads"
|
return u"Goodreads"
|
||||||
|
elif format_type == "babelio":
|
||||||
|
return u"Babelio"
|
||||||
elif format_type == "google":
|
elif format_type == "google":
|
||||||
return u"Google Books"
|
return u"Google Books"
|
||||||
elif format_type == "kobo":
|
elif format_type == "kobo":
|
||||||
@@ -140,6 +151,8 @@ class Identifiers(Base):
|
|||||||
return u"https://dx.doi.org/{0}".format(self.val)
|
return u"https://dx.doi.org/{0}".format(self.val)
|
||||||
elif format_type == "goodreads":
|
elif format_type == "goodreads":
|
||||||
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
|
return u"https://www.goodreads.com/book/show/{0}".format(self.val)
|
||||||
|
elif format_type == "babelio":
|
||||||
|
return u"https://www.babelio.com/livres/titre/{0}".format(self.val)
|
||||||
elif format_type == "douban":
|
elif format_type == "douban":
|
||||||
return u"https://book.douban.com/subject/{0}".format(self.val)
|
return u"https://book.douban.com/subject/{0}".format(self.val)
|
||||||
elif format_type == "google":
|
elif format_type == "google":
|
||||||
@@ -154,10 +167,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):
|
||||||
@@ -326,7 +337,6 @@ class Books(Base):
|
|||||||
has_cover = Column(Integer, default=0)
|
has_cover = Column(Integer, default=0)
|
||||||
uuid = Column(String)
|
uuid = Column(String)
|
||||||
isbn = Column(String(collation='NOCASE'), default="")
|
isbn = Column(String(collation='NOCASE'), default="")
|
||||||
# Iccn = Column(String(collation='NOCASE'), default="")
|
|
||||||
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')
|
||||||
@@ -384,14 +394,14 @@ class Custom_Columns(Base):
|
|||||||
|
|
||||||
class AlchemyEncoder(json.JSONEncoder):
|
class AlchemyEncoder(json.JSONEncoder):
|
||||||
|
|
||||||
def default(self, obj):
|
def default(self, o):
|
||||||
if isinstance(obj.__class__, DeclarativeMeta):
|
if isinstance(o.__class__, DeclarativeMeta):
|
||||||
# an SQLAlchemy class
|
# an SQLAlchemy class
|
||||||
fields = {}
|
fields = {}
|
||||||
for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
|
for field in [x for x in dir(o) if not x.startswith('_') and x != 'metadata' and x!="password"]:
|
||||||
if field == 'books':
|
if field == 'books':
|
||||||
continue
|
continue
|
||||||
data = obj.__getattribute__(field)
|
data = o.__getattribute__(field)
|
||||||
try:
|
try:
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
data = data.replace("'", "\'")
|
data = data.replace("'", "\'")
|
||||||
@@ -411,12 +421,12 @@ class AlchemyEncoder(json.JSONEncoder):
|
|||||||
else:
|
else:
|
||||||
json.dumps(data)
|
json.dumps(data)
|
||||||
fields[field] = data
|
fields[field] = data
|
||||||
except:
|
except Exception:
|
||||||
fields[field] = ""
|
fields[field] = ""
|
||||||
# a json-encodable dict
|
# a json-encodable dict
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
return json.JSONEncoder.default(self, obj)
|
return json.JSONEncoder.default(self, o)
|
||||||
|
|
||||||
|
|
||||||
class CalibreDB():
|
class CalibreDB():
|
||||||
@@ -437,12 +447,82 @@ class CalibreDB():
|
|||||||
|
|
||||||
self.instances.add(self)
|
self.instances.add(self)
|
||||||
|
|
||||||
|
|
||||||
def initSession(self, expire_on_commit=True):
|
def initSession(self, expire_on_commit=True):
|
||||||
self.session = self.session_factory()
|
self.session = self.session_factory()
|
||||||
self.session.expire_on_commit = expire_on_commit
|
self.session.expire_on_commit = expire_on_commit
|
||||||
self.update_title_sort(self.config)
|
self.update_title_sort(self.config)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_db_cc_classes(self, cc):
|
||||||
|
cc_ids = []
|
||||||
|
books_custom_column_links = {}
|
||||||
|
for row in cc:
|
||||||
|
if row.datatype not in cc_exceptions:
|
||||||
|
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)
|
||||||
|
if row.datatype in ['rating', 'text', 'enumeration']:
|
||||||
|
books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link',
|
||||||
|
Base.metadata,
|
||||||
|
Column('book', Integer, ForeignKey('books.id'),
|
||||||
|
primary_key=True),
|
||||||
|
Column('value', Integer,
|
||||||
|
ForeignKey('custom_column_' +
|
||||||
|
str(row.id) + '.id'),
|
||||||
|
primary_key=True)
|
||||||
|
)
|
||||||
|
cc_ids.append([row.id, row.datatype])
|
||||||
|
|
||||||
|
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
||||||
|
'id': Column(Integer, primary_key=True)}
|
||||||
|
if row.datatype == 'float':
|
||||||
|
ccdict['value'] = Column(Float)
|
||||||
|
elif row.datatype == 'int':
|
||||||
|
ccdict['value'] = Column(Integer)
|
||||||
|
elif row.datatype == 'datetime':
|
||||||
|
ccdict['value'] = Column(TIMESTAMP)
|
||||||
|
elif row.datatype == 'bool':
|
||||||
|
ccdict['value'] = Column(Boolean)
|
||||||
|
else:
|
||||||
|
ccdict['value'] = Column(String)
|
||||||
|
if row.datatype in ['float', 'int', 'bool', 'datetime', 'comments']:
|
||||||
|
ccdict['book'] = Column(Integer, ForeignKey('books.id'))
|
||||||
|
cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
|
||||||
|
|
||||||
|
for cc_id in cc_ids:
|
||||||
|
if cc_id[1] in ['bool', 'int', 'float', 'datetime', 'comments']:
|
||||||
|
setattr(Books,
|
||||||
|
'custom_column_' + str(cc_id[0]),
|
||||||
|
relationship(cc_classes[cc_id[0]],
|
||||||
|
primaryjoin=(
|
||||||
|
Books.id == cc_classes[cc_id[0]].book),
|
||||||
|
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:
|
||||||
|
setattr(Books,
|
||||||
|
'custom_column_' + str(cc_id[0]),
|
||||||
|
relationship(cc_classes[cc_id[0]],
|
||||||
|
secondary=books_custom_column_links[cc_id[0]],
|
||||||
|
backref='books'))
|
||||||
|
|
||||||
|
return cc_classes
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_db(cls, config, app_db_path):
|
def setup_db(cls, config, app_db_path):
|
||||||
cls.config = config
|
cls.config = config
|
||||||
@@ -465,84 +545,24 @@ class CalibreDB():
|
|||||||
isolation_level="SERIALIZABLE",
|
isolation_level="SERIALIZABLE",
|
||||||
connect_args={'check_same_thread': False},
|
connect_args={'check_same_thread': False},
|
||||||
poolclass=StaticPool)
|
poolclass=StaticPool)
|
||||||
cls.engine.execute("attach database '{}' as calibre;".format(dbpath))
|
with cls.engine.begin() as connection:
|
||||||
cls.engine.execute("attach database '{}' as app_settings;".format(app_db_path))
|
connection.execute(text("attach database '{}' as calibre;".format(dbpath)))
|
||||||
|
connection.execute(text("attach database '{}' as app_settings;".format(app_db_path)))
|
||||||
|
|
||||||
conn = cls.engine.connect()
|
conn = cls.engine.connect()
|
||||||
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
config.invalidate(e)
|
config.invalidate(ex)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
config.db_configured = True
|
config.db_configured = True
|
||||||
|
|
||||||
if not cc_classes:
|
if not cc_classes:
|
||||||
|
try:
|
||||||
cc = conn.execute("SELECT id, datatype FROM custom_columns")
|
cc = conn.execute("SELECT id, datatype FROM custom_columns")
|
||||||
|
cls.setup_db_cc_classes(cc)
|
||||||
cc_ids = []
|
except OperationalError as e:
|
||||||
books_custom_column_links = {}
|
log.debug_or_exception(e)
|
||||||
for row in cc:
|
|
||||||
if row.datatype not in cc_exceptions:
|
|
||||||
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'),
|
|
||||||
primary_key=True),
|
|
||||||
Column('value', Integer,
|
|
||||||
ForeignKey('custom_column_' +
|
|
||||||
str(row.id) + '.id'),
|
|
||||||
primary_key=True)
|
|
||||||
)
|
|
||||||
cc_ids.append([row.id, row.datatype])
|
|
||||||
|
|
||||||
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
|
|
||||||
'id': Column(Integer, primary_key=True)}
|
|
||||||
if row.datatype == 'float':
|
|
||||||
ccdict['value'] = Column(Float)
|
|
||||||
elif row.datatype == 'int':
|
|
||||||
ccdict['value'] = Column(Integer)
|
|
||||||
elif row.datatype == 'bool':
|
|
||||||
ccdict['value'] = Column(Boolean)
|
|
||||||
else:
|
|
||||||
ccdict['value'] = Column(String)
|
|
||||||
if row.datatype in ['float', 'int', 'bool']:
|
|
||||||
ccdict['book'] = Column(Integer, ForeignKey('books.id'))
|
|
||||||
cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict)
|
|
||||||
|
|
||||||
for cc_id in cc_ids:
|
|
||||||
if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'):
|
|
||||||
setattr(Books,
|
|
||||||
'custom_column_' + str(cc_id[0]),
|
|
||||||
relationship(cc_classes[cc_id[0]],
|
|
||||||
primaryjoin=(
|
|
||||||
Books.id == cc_classes[cc_id[0]].book),
|
|
||||||
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:
|
|
||||||
setattr(Books,
|
|
||||||
'custom_column_' + str(cc_id[0]),
|
|
||||||
relationship(cc_classes[cc_id[0]],
|
|
||||||
secondary=books_custom_column_links[cc_id[0]],
|
|
||||||
backref='books'))
|
|
||||||
|
|
||||||
cls.session_factory = scoped_session(sessionmaker(autocommit=False,
|
cls.session_factory = scoped_session(sessionmaker(autocommit=False,
|
||||||
autoflush=True,
|
autoflush=True,
|
||||||
@@ -563,8 +583,8 @@ class CalibreDB():
|
|||||||
def get_book_by_uuid(self, book_uuid):
|
def get_book_by_uuid(self, book_uuid):
|
||||||
return self.session.query(Books).filter(Books.uuid == book_uuid).first()
|
return self.session.query(Books).filter(Books.uuid == book_uuid).first()
|
||||||
|
|
||||||
def get_book_format(self, book_id, format):
|
def get_book_format(self, book_id, file_format):
|
||||||
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == format).first()
|
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
|
||||||
|
|
||||||
# Language and content filters for displaying in the UI
|
# Language and content filters for displaying in the UI
|
||||||
def common_filters(self, allow_show_archived=False):
|
def common_filters(self, allow_show_archived=False):
|
||||||
@@ -589,6 +609,7 @@ class CalibreDB():
|
|||||||
neg_content_tags_filter = false() if negtags_list == [''] else Books.tags.any(Tags.name.in_(negtags_list))
|
neg_content_tags_filter = false() if negtags_list == [''] else Books.tags.any(Tags.name.in_(negtags_list))
|
||||||
pos_content_tags_filter = true() if postags_list == [''] else Books.tags.any(Tags.name.in_(postags_list))
|
pos_content_tags_filter = true() if postags_list == [''] else Books.tags.any(Tags.name.in_(postags_list))
|
||||||
if self.config.config_restricted_column:
|
if self.config.config_restricted_column:
|
||||||
|
try:
|
||||||
pos_cc_list = current_user.allowed_column_value.split(',')
|
pos_cc_list = current_user.allowed_column_value.split(',')
|
||||||
pos_content_cc_filter = true() if pos_cc_list == [''] else \
|
pos_content_cc_filter = true() if pos_cc_list == [''] else \
|
||||||
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
||||||
@@ -597,12 +618,37 @@ class CalibreDB():
|
|||||||
neg_content_cc_filter = false() if neg_cc_list == [''] else \
|
neg_content_cc_filter = false() if neg_cc_list == [''] else \
|
||||||
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
getattr(Books, 'custom_column_' + str(self.config.config_restricted_column)). \
|
||||||
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
|
any(cc_classes[self.config.config_restricted_column].value.in_(neg_cc_list))
|
||||||
|
except (KeyError, AttributeError):
|
||||||
|
pos_content_cc_filter = false()
|
||||||
|
neg_content_cc_filter = true()
|
||||||
|
log.error(u"Custom Column No.%d is not existing in calibre database",
|
||||||
|
self.config.config_restricted_column)
|
||||||
|
flash(_("Custom Column No.%(column)d is not existing in calibre database",
|
||||||
|
column=self.config.config_restricted_column),
|
||||||
|
category="error")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pos_content_cc_filter = true()
|
pos_content_cc_filter = true()
|
||||||
neg_content_cc_filter = false()
|
neg_content_cc_filter = false()
|
||||||
return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter,
|
return and_(lang_filter, pos_content_tags_filter, ~neg_content_tags_filter,
|
||||||
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
|
pos_content_cc_filter, ~neg_content_cc_filter, archived_filter)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_checkbox_sorted(inputlist, state, offset, limit, order):
|
||||||
|
outcome = list()
|
||||||
|
elementlist = {ele.id: ele for ele in inputlist}
|
||||||
|
for entry in state:
|
||||||
|
try:
|
||||||
|
outcome.append(elementlist[entry])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
del elementlist[entry]
|
||||||
|
for entry in elementlist:
|
||||||
|
outcome.append(elementlist[entry])
|
||||||
|
if order == "asc":
|
||||||
|
outcome.reverse()
|
||||||
|
return outcome[offset:offset + limit]
|
||||||
|
|
||||||
# Fill indexpage with all requested data from database
|
# Fill indexpage with all requested data from database
|
||||||
def fill_indexpage(self, page, pagesize, database, db_filter, order, *join):
|
def fill_indexpage(self, page, pagesize, database, db_filter, order, *join):
|
||||||
return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
|
return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
|
||||||
@@ -614,22 +660,29 @@ class CalibreDB():
|
|||||||
randm = self.session.query(Books) \
|
randm = self.session.query(Books) \
|
||||||
.filter(self.common_filters(allow_show_archived)) \
|
.filter(self.common_filters(allow_show_archived)) \
|
||||||
.order_by(func.random()) \
|
.order_by(func.random()) \
|
||||||
.limit(self.config.config_random_books)
|
.limit(self.config.config_random_books).all()
|
||||||
else:
|
else:
|
||||||
randm = false()
|
randm = false()
|
||||||
off = int(int(pagesize) * (page - 1))
|
off = int(int(pagesize) * (page - 1))
|
||||||
query = self.session.query(database) \
|
query = self.session.query(database)
|
||||||
.join(*join, isouter=True) \
|
if len(join) == 3:
|
||||||
.filter(db_filter) \
|
query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
|
||||||
|
elif len(join) == 2:
|
||||||
|
query = query.outerjoin(join[0], join[1])
|
||||||
|
elif len(join) == 1:
|
||||||
|
query = query.outerjoin(join[0])
|
||||||
|
query = query.filter(db_filter)\
|
||||||
.filter(self.common_filters(allow_show_archived))
|
.filter(self.common_filters(allow_show_archived))
|
||||||
|
entries = list()
|
||||||
|
pagination = list()
|
||||||
try:
|
try:
|
||||||
pagination = Pagination(page, pagesize,
|
pagination = Pagination(page, pagesize,
|
||||||
len(query.all()))
|
len(query.all()))
|
||||||
entries = query.order_by(*order).offset(off).limit(pagesize).all()
|
entries = query.order_by(*order).offset(off).limit(pagesize).all()
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
logger.debug(e)
|
log.debug_or_exception(ex)
|
||||||
for book in entries:
|
#for book in entries:
|
||||||
book = self.order_authors(book)
|
# book = self.order_authors(book)
|
||||||
return entries, randm, pagination
|
return entries, randm, pagination
|
||||||
|
|
||||||
# Orders all Authors in the list according to authors sort
|
# Orders all Authors in the list according to authors sort
|
||||||
@@ -669,23 +722,33 @@ class CalibreDB():
|
|||||||
return self.session.query(Books) \
|
return self.session.query(Books) \
|
||||||
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
|
.filter(and_(Books.authors.any(and_(*q)), func.lower(Books.title).ilike("%" + title + "%"))).first()
|
||||||
|
|
||||||
# read search results from calibre-database and return it (function is used for feed and simple search
|
def search_query(self, term, *join):
|
||||||
def get_search_results(self, term, offset=None, order=None, limit=None):
|
|
||||||
order = order or [Books.sort]
|
|
||||||
pagination = None
|
|
||||||
term.strip().lower()
|
term.strip().lower()
|
||||||
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
self.session.connection().connection.connection.create_function("lower", 1, lcase)
|
||||||
q = list()
|
q = list()
|
||||||
authorterms = re.split("[, ]+", term)
|
authorterms = re.split("[, ]+", term)
|
||||||
for authorterm in authorterms:
|
for authorterm in authorterms:
|
||||||
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
|
q.append(Books.authors.any(func.lower(Authors.name).ilike("%" + authorterm + "%")))
|
||||||
result = self.session.query(Books).filter(self.common_filters(True)).filter(
|
query = self.session.query(Books)
|
||||||
|
if len(join) == 3:
|
||||||
|
query = query.outerjoin(join[0], join[1]).outerjoin(join[2])
|
||||||
|
elif len(join) == 2:
|
||||||
|
query = query.outerjoin(join[0], join[1])
|
||||||
|
elif len(join) == 1:
|
||||||
|
query = query.outerjoin(join[0])
|
||||||
|
return query.filter(self.common_filters(True)).filter(
|
||||||
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
|
or_(Books.tags.any(func.lower(Tags.name).ilike("%" + term + "%")),
|
||||||
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
|
Books.series.any(func.lower(Series.name).ilike("%" + term + "%")),
|
||||||
Books.authors.any(and_(*q)),
|
Books.authors.any(and_(*q)),
|
||||||
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
|
Books.publishers.any(func.lower(Publishers.name).ilike("%" + term + "%")),
|
||||||
func.lower(Books.title).ilike("%" + term + "%")
|
func.lower(Books.title).ilike("%" + term + "%")
|
||||||
)).order_by(*order).all()
|
))
|
||||||
|
|
||||||
|
# read search results from calibre-database and return it (function is used for feed and simple search
|
||||||
|
def get_search_results(self, term, offset=None, order=None, limit=None, *join):
|
||||||
|
order = order or [Books.sort]
|
||||||
|
pagination = None
|
||||||
|
result = self.search_query(term, *join).order_by(*order).all()
|
||||||
result_count = len(result)
|
result_count = len(result)
|
||||||
if offset != None and limit != None:
|
if offset != None and limit != None:
|
||||||
offset = int(offset)
|
offset = int(offset)
|
||||||
@@ -740,7 +803,7 @@ class CalibreDB():
|
|||||||
if old_session:
|
if old_session:
|
||||||
try:
|
try:
|
||||||
old_session.close()
|
old_session.close()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if old_session.bind:
|
if old_session.bind:
|
||||||
try:
|
try:
|
||||||
@@ -771,7 +834,7 @@ class CalibreDB():
|
|||||||
def lcase(s):
|
def lcase(s):
|
||||||
try:
|
try:
|
||||||
return unidecode.unidecode(s.lower())
|
return unidecode.unidecode(s.lower())
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
return s.lower()
|
return s.lower()
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import json
|
|||||||
from shutil import copyfile
|
from shutil import copyfile
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from babel import Locale as LC
|
||||||
|
from babel.core import UnknownLocaleError
|
||||||
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
@@ -68,17 +70,7 @@ def edit_required(f):
|
|||||||
|
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
def search_objects_remove(db_book_object, db_type, input_elements):
|
||||||
# Modifies different Database objects, first check if elements have to be added to database, than check
|
|
||||||
# if elements have to be deleted, because they are no longer used
|
|
||||||
def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
|
|
||||||
# passing input_elements not as a list may lead to undesired results
|
|
||||||
if not isinstance(input_elements, list):
|
|
||||||
raise TypeError(str(input_elements) + " should be passed as a list")
|
|
||||||
changed = False
|
|
||||||
input_elements = [x for x in input_elements if x != '']
|
|
||||||
# we have all input element (authors, series, tags) names now
|
|
||||||
# 1. search for elements to remove
|
|
||||||
del_elements = []
|
del_elements = []
|
||||||
for c_elements in db_book_object:
|
for c_elements in db_book_object:
|
||||||
found = False
|
found = False
|
||||||
@@ -96,7 +88,10 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
# if the element was not found in the new list, add it to remove list
|
# if the element was not found in the new list, add it to remove list
|
||||||
if not found:
|
if not found:
|
||||||
del_elements.append(c_elements)
|
del_elements.append(c_elements)
|
||||||
# 2. search for elements that need to be added
|
return del_elements
|
||||||
|
|
||||||
|
|
||||||
|
def search_objects_add(db_book_object, db_type, input_elements):
|
||||||
add_elements = []
|
add_elements = []
|
||||||
for inp_element in input_elements:
|
for inp_element in input_elements:
|
||||||
found = False
|
found = False
|
||||||
@@ -112,15 +107,21 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
add_elements.append(inp_element)
|
add_elements.append(inp_element)
|
||||||
# if there are elements to remove, we remove them now
|
return add_elements
|
||||||
|
|
||||||
|
|
||||||
|
def remove_objects(db_book_object, db_session, del_elements):
|
||||||
|
changed = False
|
||||||
if len(del_elements) > 0:
|
if len(del_elements) > 0:
|
||||||
for del_element in del_elements:
|
for del_element in del_elements:
|
||||||
db_book_object.remove(del_element)
|
db_book_object.remove(del_element)
|
||||||
changed = True
|
changed = True
|
||||||
if len(del_element.books) == 0:
|
if len(del_element.books) == 0:
|
||||||
db_session.delete(del_element)
|
db_session.delete(del_element)
|
||||||
# if there are elements to add, we add them now!
|
return changed
|
||||||
if len(add_elements) > 0:
|
|
||||||
|
def add_objects(db_book_object, db_object, db_session, db_type, add_elements):
|
||||||
|
changed = False
|
||||||
if db_type == 'languages':
|
if db_type == 'languages':
|
||||||
db_filter = db_object.lang_code
|
db_filter = db_object.lang_code
|
||||||
elif db_type == 'custom':
|
elif db_type == 'custom':
|
||||||
@@ -147,9 +148,18 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
db_session.add(new_element)
|
db_session.add(new_element)
|
||||||
db_book_object.append(new_element)
|
db_book_object.append(new_element)
|
||||||
else:
|
else:
|
||||||
|
db_element = create_objects_for_addition(db_element, add_element, db_type)
|
||||||
|
changed = True
|
||||||
|
# add element to book
|
||||||
|
changed = True
|
||||||
|
db_book_object.append(db_element)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
|
def create_objects_for_addition(db_element, add_element, db_type):
|
||||||
if db_type == 'custom':
|
if db_type == 'custom':
|
||||||
if db_element.value != add_element:
|
if db_element.value != add_element:
|
||||||
new_element.value = add_element
|
db_element.value = add_element # ToDo: Before new_element, but this is not plausible
|
||||||
elif db_type == 'languages':
|
elif db_type == 'languages':
|
||||||
if db_element.lang_code != add_element:
|
if db_element.lang_code != add_element:
|
||||||
db_element.lang_code = add_element
|
db_element.lang_code = add_element
|
||||||
@@ -167,9 +177,26 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session
|
|||||||
db_element.sort = None
|
db_element.sort = None
|
||||||
elif db_element.name != add_element:
|
elif db_element.name != add_element:
|
||||||
db_element.name = add_element
|
db_element.name = add_element
|
||||||
# add element to book
|
return db_element
|
||||||
changed = True
|
|
||||||
db_book_object.append(db_element)
|
|
||||||
|
# Modifies different Database objects, first check if elements if elements have to be deleted,
|
||||||
|
# because they are no longer used, than check if elements have to be added to database
|
||||||
|
def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type):
|
||||||
|
# passing input_elements not as a list may lead to undesired results
|
||||||
|
if not isinstance(input_elements, list):
|
||||||
|
raise TypeError(str(input_elements) + " should be passed as a list")
|
||||||
|
input_elements = [x for x in input_elements if x != '']
|
||||||
|
# we have all input element (authors, series, tags) names now
|
||||||
|
# 1. search for elements to remove
|
||||||
|
del_elements = search_objects_remove(db_book_object, db_type, input_elements)
|
||||||
|
# 2. search for elements that need to be added
|
||||||
|
add_elements = search_objects_add(db_book_object, db_type, input_elements)
|
||||||
|
# if there are elements to remove, we remove them now
|
||||||
|
changed = remove_objects(db_book_object, db_session, del_elements)
|
||||||
|
# if there are elements to add, we add them now!
|
||||||
|
if len(add_elements) > 0:
|
||||||
|
changed |= add_objects(db_book_object, db_object, db_session, db_type, add_elements)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
@@ -202,40 +229,17 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
|
|||||||
@editbook.route("/ajax/delete/<int:book_id>")
|
@editbook.route("/ajax/delete/<int:book_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def delete_book_from_details(book_id):
|
def delete_book_from_details(book_id):
|
||||||
return Response(delete_book(book_id,"", True), mimetype='application/json')
|
return Response(delete_book(book_id, "", True), mimetype='application/json')
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""})
|
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""})
|
||||||
@editbook.route("/delete/<int:book_id>/<string:book_format>")
|
@editbook.route("/delete/<int:book_id>/<string:book_format>")
|
||||||
@login_required
|
@login_required
|
||||||
def delete_book_ajax(book_id, book_format):
|
def delete_book_ajax(book_id, book_format):
|
||||||
return delete_book(book_id,book_format, False)
|
return delete_book(book_id, book_format, False)
|
||||||
|
|
||||||
def delete_book(book_id, book_format, jsonResponse):
|
|
||||||
warning = {}
|
def delete_whole_book(book_id, book):
|
||||||
if current_user.role_delete_books():
|
|
||||||
book = calibre_db.get_book(book_id)
|
|
||||||
if book:
|
|
||||||
try:
|
|
||||||
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
|
|
||||||
if not result:
|
|
||||||
if jsonResponse:
|
|
||||||
return json.dumps({"location": url_for("editbook.edit_book"),
|
|
||||||
"type": "alert",
|
|
||||||
"format": "",
|
|
||||||
"error": error}),
|
|
||||||
else:
|
|
||||||
flash(error, category="error")
|
|
||||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
|
||||||
if error:
|
|
||||||
if jsonResponse:
|
|
||||||
warning = {"location": url_for("editbook.edit_book"),
|
|
||||||
"type": "warning",
|
|
||||||
"format": "",
|
|
||||||
"error": error}
|
|
||||||
else:
|
|
||||||
flash(error, category="warning")
|
|
||||||
if not book_format:
|
|
||||||
# delete book from Shelfs, Downloads, Read list
|
# delete book from Shelfs, Downloads, Read list
|
||||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
|
ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book_id).delete()
|
||||||
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
|
ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id).delete()
|
||||||
@@ -250,7 +254,7 @@ def delete_book(book_id, book_format, jsonResponse):
|
|||||||
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
|
modify_database_object([u''], book.languages, db.Languages, calibre_db.session, 'languages')
|
||||||
modify_database_object([u''], book.publishers, db.Publishers, calibre_db.session, 'publishers')
|
modify_database_object([u''], book.publishers, db.Publishers, calibre_db.session, 'publishers')
|
||||||
|
|
||||||
cc = calibre_db.session.query(db.Custom_Columns).\
|
cc = calibre_db.session.query(db.Custom_Columns). \
|
||||||
filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
for c in cc:
|
for c in cc:
|
||||||
cc_string = "custom_column_" + str(c.id)
|
cc_string = "custom_column_" + str(c.id)
|
||||||
@@ -279,16 +283,9 @@ def delete_book(book_id, book_format, jsonResponse):
|
|||||||
modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id],
|
modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id],
|
||||||
calibre_db.session, 'custom')
|
calibre_db.session, 'custom')
|
||||||
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
|
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
|
||||||
else:
|
|
||||||
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
|
|
||||||
filter(db.Data.format == book_format).delete()
|
def render_delete_book_result(book_format, jsonResponse, warning, book_id):
|
||||||
calibre_db.session.commit()
|
|
||||||
except Exception as e:
|
|
||||||
log.debug_or_exception(e)
|
|
||||||
calibre_db.session.rollback()
|
|
||||||
else:
|
|
||||||
# book not found
|
|
||||||
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
|
||||||
if book_format:
|
if book_format:
|
||||||
if jsonResponse:
|
if jsonResponse:
|
||||||
return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id),
|
return json.dumps([warning, {"location": url_for("editbook.edit_book", book_id=book_id),
|
||||||
@@ -309,11 +306,59 @@ def delete_book(book_id, book_format, jsonResponse):
|
|||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_book(book_id, book_format, jsonResponse):
|
||||||
|
warning = {}
|
||||||
|
if current_user.role_delete_books():
|
||||||
|
book = calibre_db.get_book(book_id)
|
||||||
|
if book:
|
||||||
|
try:
|
||||||
|
result, error = helper.delete_book(book, config.config_calibre_dir, book_format=book_format.upper())
|
||||||
|
if not result:
|
||||||
|
if jsonResponse:
|
||||||
|
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id),
|
||||||
|
"type": "danger",
|
||||||
|
"format": "",
|
||||||
|
"message": error}])
|
||||||
|
else:
|
||||||
|
flash(error, category="error")
|
||||||
|
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||||
|
if error:
|
||||||
|
if jsonResponse:
|
||||||
|
warning = {"location": url_for("editbook.edit_book", book_id=book_id),
|
||||||
|
"type": "warning",
|
||||||
|
"format": "",
|
||||||
|
"message": error}
|
||||||
|
else:
|
||||||
|
flash(error, category="warning")
|
||||||
|
if not book_format:
|
||||||
|
delete_whole_book(book_id, book)
|
||||||
|
else:
|
||||||
|
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
|
||||||
|
filter(db.Data.format == book_format).delete()
|
||||||
|
calibre_db.session.commit()
|
||||||
|
except Exception as ex:
|
||||||
|
log.debug_or_exception(ex)
|
||||||
|
calibre_db.session.rollback()
|
||||||
|
if jsonResponse:
|
||||||
|
return json.dumps([{"location": url_for("editbook.edit_book", book_id=book_id),
|
||||||
|
"type": "danger",
|
||||||
|
"format": "",
|
||||||
|
"message": ex}])
|
||||||
|
else:
|
||||||
|
flash(str(ex), category="error")
|
||||||
|
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# book not found
|
||||||
|
log.error('Book with id "%s" could not be deleted: not found', book_id)
|
||||||
|
return render_delete_book_result(book_format, jsonResponse, warning, book_id)
|
||||||
|
|
||||||
|
|
||||||
def render_edit_book(book_id):
|
def render_edit_book(book_id):
|
||||||
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
if not book:
|
if not book:
|
||||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
|
||||||
for lang in book.languages:
|
for lang in book.languages:
|
||||||
@@ -394,6 +439,9 @@ def edit_book_series_index(series_index, book):
|
|||||||
# Add default series_index to book
|
# Add default series_index to book
|
||||||
modif_date = False
|
modif_date = False
|
||||||
series_index = series_index or '1'
|
series_index = series_index or '1'
|
||||||
|
if not series_index.replace('.', '', 1).isdigit():
|
||||||
|
flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning")
|
||||||
|
return False
|
||||||
if book.series_index != series_index:
|
if book.series_index != series_index:
|
||||||
book.series_index = series_index
|
book.series_index = series_index
|
||||||
modif_date = True
|
modif_date = True
|
||||||
@@ -413,7 +461,7 @@ def edit_book_comments(comments, book):
|
|||||||
return modif_date
|
return modif_date
|
||||||
|
|
||||||
|
|
||||||
def edit_book_languages(languages, book, upload=False):
|
def edit_book_languages(languages, book, upload=False, invalid=None):
|
||||||
input_languages = languages.split(',')
|
input_languages = languages.split(',')
|
||||||
unknown_languages = []
|
unknown_languages = []
|
||||||
if not upload:
|
if not upload:
|
||||||
@@ -422,6 +470,9 @@ def edit_book_languages(languages, book, upload=False):
|
|||||||
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
|
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
|
||||||
for l in unknown_languages:
|
for l in unknown_languages:
|
||||||
log.error('%s is not a valid language', l)
|
log.error('%s is not a valid language', l)
|
||||||
|
if isinstance(invalid, list):
|
||||||
|
invalid.append(l)
|
||||||
|
else:
|
||||||
flash(_(u"%(langname)s is not a valid language", langname=l), category="warning")
|
flash(_(u"%(langname)s is not a valid language", langname=l), category="warning")
|
||||||
# ToDo: Not working correct
|
# ToDo: Not working correct
|
||||||
if upload and len(input_l) == 1:
|
if upload and len(input_l) == 1:
|
||||||
@@ -435,10 +486,10 @@ def edit_book_languages(languages, book, upload=False):
|
|||||||
return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages')
|
return modify_database_object(input_l, book.languages, db.Languages, calibre_db.session, 'languages')
|
||||||
|
|
||||||
|
|
||||||
def edit_book_publisher(to_save, book):
|
def edit_book_publisher(publishers, book):
|
||||||
changed = False
|
changed = False
|
||||||
if to_save["publisher"]:
|
if publishers:
|
||||||
publisher = to_save["publisher"].rstrip().strip()
|
publisher = publishers.rstrip().strip()
|
||||||
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
|
if len(book.publishers) == 0 or (len(book.publishers) > 0 and publisher != book.publishers[0].name):
|
||||||
changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session,
|
changed |= modify_database_object([publisher], book.publishers, db.Publishers, calibre_db.session,
|
||||||
'publisher')
|
'publisher')
|
||||||
@@ -447,22 +498,19 @@ def edit_book_publisher(to_save, book):
|
|||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def edit_cc_data(book_id, book, to_save):
|
def edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string):
|
||||||
changed = False
|
changed = False
|
||||||
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
|
||||||
for c in cc:
|
|
||||||
cc_string = "custom_column_" + str(c.id)
|
|
||||||
if not c.is_multiple:
|
|
||||||
if len(getattr(book, cc_string)) > 0:
|
|
||||||
cc_db_value = getattr(book, cc_string)[0].value
|
|
||||||
else:
|
|
||||||
cc_db_value = None
|
|
||||||
if to_save[cc_string].strip():
|
|
||||||
if c.datatype == 'int' or c.datatype == 'bool' or c.datatype == 'float':
|
|
||||||
if to_save[cc_string] == 'None':
|
if to_save[cc_string] == 'None':
|
||||||
to_save[cc_string] = None
|
to_save[cc_string] = None
|
||||||
elif c.datatype == 'bool':
|
elif c.datatype == 'bool':
|
||||||
to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0
|
to_save[cc_string] = 1 if to_save[cc_string] == 'True' else 0
|
||||||
|
elif c.datatype == 'comments':
|
||||||
|
to_save[cc_string] = Markup(to_save[cc_string]).unescape()
|
||||||
|
elif c.datatype == 'datetime':
|
||||||
|
try:
|
||||||
|
to_save[cc_string] = datetime.strptime(to_save[cc_string], "%Y-%m-%d")
|
||||||
|
except ValueError:
|
||||||
|
to_save[cc_string] = db.Books.DEFAULT_PUBDATE
|
||||||
|
|
||||||
if to_save[cc_string] != cc_db_value:
|
if to_save[cc_string] != cc_db_value:
|
||||||
if cc_db_value is not None:
|
if cc_db_value is not None:
|
||||||
@@ -479,8 +527,11 @@ def edit_cc_data(book_id, book, to_save):
|
|||||||
new_cc = cc_class(value=to_save[cc_string], book=book_id)
|
new_cc = cc_class(value=to_save[cc_string], book=book_id)
|
||||||
calibre_db.session.add(new_cc)
|
calibre_db.session.add(new_cc)
|
||||||
changed = True
|
changed = True
|
||||||
|
return changed, to_save
|
||||||
|
|
||||||
else:
|
|
||||||
|
def edit_cc_data_string(book, c, to_save, cc_db_value, cc_string):
|
||||||
|
changed = False
|
||||||
if c.datatype == 'rating':
|
if c.datatype == 'rating':
|
||||||
to_save[cc_string] = str(int(float(to_save[cc_string]) * 2))
|
to_save[cc_string] = str(int(float(to_save[cc_string]) * 2))
|
||||||
if to_save[cc_string].strip() != cc_db_value:
|
if to_save[cc_string].strip() != cc_db_value:
|
||||||
@@ -504,6 +555,24 @@ def edit_cc_data(book_id, book, to_save):
|
|||||||
cc_class.value == to_save[cc_string].strip()).first()
|
cc_class.value == to_save[cc_string].strip()).first()
|
||||||
# add cc value to book
|
# add cc value to book
|
||||||
getattr(book, cc_string).append(new_cc)
|
getattr(book, cc_string).append(new_cc)
|
||||||
|
return changed, to_save
|
||||||
|
|
||||||
|
|
||||||
|
def edit_cc_data(book_id, book, to_save):
|
||||||
|
changed = False
|
||||||
|
cc = calibre_db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all()
|
||||||
|
for c in cc:
|
||||||
|
cc_string = "custom_column_" + str(c.id)
|
||||||
|
if not c.is_multiple:
|
||||||
|
if len(getattr(book, cc_string)) > 0:
|
||||||
|
cc_db_value = getattr(book, cc_string)[0].value
|
||||||
|
else:
|
||||||
|
cc_db_value = None
|
||||||
|
if to_save[cc_string].strip():
|
||||||
|
if c.datatype in ['int', 'bool', 'float', "datetime", "comments"]:
|
||||||
|
changed, to_save = edit_cc_data_value(book_id, book, c, to_save, cc_db_value, cc_string)
|
||||||
|
else:
|
||||||
|
changed, to_save = edit_cc_data_string(book, c, to_save, cc_db_value, cc_string)
|
||||||
else:
|
else:
|
||||||
if cc_db_value is not None:
|
if cc_db_value is not None:
|
||||||
# remove old cc_val
|
# remove old cc_val
|
||||||
@@ -577,7 +646,7 @@ def upload_single_file(request, book, book_id):
|
|||||||
|
|
||||||
# Queue uploader info
|
# Queue uploader info
|
||||||
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
|
uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title)
|
||||||
WorkerThread.add(current_user.nickname, TaskUpload(
|
WorkerThread.add(current_user.name, TaskUpload(
|
||||||
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>"))
|
"<a href=\"" + url_for('web.show_book', book_id=book.id) + "\">" + uploadText + "</a>"))
|
||||||
|
|
||||||
return uploader.process(
|
return uploader.process(
|
||||||
@@ -601,6 +670,46 @@ def upload_cover(request, book):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def handle_title_on_edit(book, book_title):
|
||||||
|
# handle book title
|
||||||
|
book_title = book_title.rstrip().strip()
|
||||||
|
if book.title != book_title:
|
||||||
|
if book_title == '':
|
||||||
|
book_title = _(u'Unknown')
|
||||||
|
book.title = book_title
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def handle_author_on_edit(book, author_name, update_stored=True):
|
||||||
|
# handle author(s)
|
||||||
|
input_authors = author_name.split('&')
|
||||||
|
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
|
||||||
|
# Remove duplicates in authors list
|
||||||
|
input_authors = helper.uniq(input_authors)
|
||||||
|
# we have all author names now
|
||||||
|
if input_authors == ['']:
|
||||||
|
input_authors = [_(u'Unknown')] # prevent empty Author
|
||||||
|
|
||||||
|
change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
||||||
|
|
||||||
|
# Search for each author if author is in database, if not, author name and sorted author name is generated new
|
||||||
|
# everything then is assembled for sorted author field in database
|
||||||
|
sort_authors_list = list()
|
||||||
|
for inp in input_authors:
|
||||||
|
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
||||||
|
if not stored_author:
|
||||||
|
stored_author = helper.get_sorted_author(inp)
|
||||||
|
else:
|
||||||
|
stored_author = stored_author.sort
|
||||||
|
sort_authors_list.append(helper.get_sorted_author(stored_author))
|
||||||
|
sort_authors = ' & '.join(sort_authors_list)
|
||||||
|
if book.author_sort != sort_authors and update_stored:
|
||||||
|
book.author_sort = sort_authors
|
||||||
|
change = True
|
||||||
|
return input_authors, change
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/admin/book/<int:book_id>", methods=['GET', 'POST'])
|
@editbook.route("/admin/book/<int:book_id>", methods=['GET', 'POST'])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@edit_required
|
@edit_required
|
||||||
@@ -618,12 +727,11 @@ def edit_book(book_id):
|
|||||||
if request.method != 'POST':
|
if request.method != 'POST':
|
||||||
return render_edit_book(book_id)
|
return render_edit_book(book_id)
|
||||||
|
|
||||||
|
|
||||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||||
|
|
||||||
# Book not found
|
# Book not found
|
||||||
if not book:
|
if not book:
|
||||||
flash(_(u"Error opening eBook. File does not exist or file is not accessible"), category="error")
|
flash(_(u"Oops! Selected book title is unavailable. File does not exist or is not accessible"), category="error")
|
||||||
return redirect(url_for("web.index"))
|
return redirect(url_for("web.index"))
|
||||||
|
|
||||||
meta = upload_single_file(request, book, book_id)
|
meta = upload_single_file(request, book, book_id)
|
||||||
@@ -636,41 +744,14 @@ def edit_book(book_id):
|
|||||||
# Update book
|
# Update book
|
||||||
edited_books_id = None
|
edited_books_id = None
|
||||||
|
|
||||||
#handle book title
|
# handle book title
|
||||||
if book.title != to_save["book_title"].rstrip().strip():
|
title_change = handle_title_on_edit(book, to_save["book_title"])
|
||||||
if to_save["book_title"] == '':
|
|
||||||
to_save["book_title"] = _(u'Unknown')
|
input_authors, authorchange = handle_author_on_edit(book, to_save["author_name"])
|
||||||
book.title = to_save["book_title"].rstrip().strip()
|
if authorchange or title_change:
|
||||||
edited_books_id = book.id
|
edited_books_id = book.id
|
||||||
modif_date = True
|
modif_date = True
|
||||||
|
|
||||||
# handle author(s)
|
|
||||||
input_authors = to_save["author_name"].split('&')
|
|
||||||
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
|
|
||||||
# Remove duplicates in authors list
|
|
||||||
input_authors = helper.uniq(input_authors)
|
|
||||||
# we have all author names now
|
|
||||||
if input_authors == ['']:
|
|
||||||
input_authors = [_(u'Unknown')] # prevent empty Author
|
|
||||||
|
|
||||||
modif_date |= modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
|
||||||
|
|
||||||
# Search for each author if author is in database, if not, authorname and sorted authorname is generated new
|
|
||||||
# everything then is assembled for sorted author field in database
|
|
||||||
sort_authors_list = list()
|
|
||||||
for inp in input_authors:
|
|
||||||
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
|
||||||
if not stored_author:
|
|
||||||
stored_author = helper.get_sorted_author(inp)
|
|
||||||
else:
|
|
||||||
stored_author = stored_author.sort
|
|
||||||
sort_authors_list.append(helper.get_sorted_author(stored_author))
|
|
||||||
sort_authors = ' & '.join(sort_authors_list)
|
|
||||||
if book.author_sort != sort_authors:
|
|
||||||
edited_books_id = book.id
|
|
||||||
book.author_sort = sort_authors
|
|
||||||
modif_date = True
|
|
||||||
|
|
||||||
if config.config_use_google_drive:
|
if config.config_use_google_drive:
|
||||||
gdriveutils.updateGdriveCalibreFromLocal()
|
gdriveutils.updateGdriveCalibreFromLocal()
|
||||||
|
|
||||||
@@ -695,10 +776,8 @@ def edit_book(book_id):
|
|||||||
|
|
||||||
# Add default series_index to book
|
# Add default series_index to book
|
||||||
modif_date |= edit_book_series_index(to_save["series_index"], book)
|
modif_date |= edit_book_series_index(to_save["series_index"], book)
|
||||||
|
|
||||||
# Handle book comments/description
|
# Handle book comments/description
|
||||||
modif_date |= edit_book_comments(to_save["description"], book)
|
modif_date |= edit_book_comments(Markup(to_save['description']).unescape(), book)
|
||||||
|
|
||||||
# Handle identifiers
|
# Handle identifiers
|
||||||
input_identifiers = identifier_list(to_save, book)
|
input_identifiers = identifier_list(to_save, book)
|
||||||
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
|
modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session)
|
||||||
@@ -707,9 +786,16 @@ def edit_book(book_id):
|
|||||||
modif_date |= modification
|
modif_date |= modification
|
||||||
# Handle book tags
|
# Handle book tags
|
||||||
modif_date |= edit_book_tags(to_save['tags'], book)
|
modif_date |= edit_book_tags(to_save['tags'], book)
|
||||||
|
|
||||||
# Handle book series
|
# Handle book series
|
||||||
modif_date |= edit_book_series(to_save["series"], book)
|
modif_date |= edit_book_series(to_save["series"], book)
|
||||||
|
# handle book publisher
|
||||||
|
modif_date |= edit_book_publisher(to_save['publisher'], book)
|
||||||
|
# handle book languages
|
||||||
|
modif_date |= edit_book_languages(to_save['languages'], book)
|
||||||
|
# handle book ratings
|
||||||
|
modif_date |= edit_book_ratings(to_save, book)
|
||||||
|
# handle cc data
|
||||||
|
modif_date |= edit_cc_data(book_id, book, to_save)
|
||||||
|
|
||||||
if to_save["pubdate"]:
|
if to_save["pubdate"]:
|
||||||
try:
|
try:
|
||||||
@@ -719,18 +805,6 @@ def edit_book(book_id):
|
|||||||
else:
|
else:
|
||||||
book.pubdate = db.Books.DEFAULT_PUBDATE
|
book.pubdate = db.Books.DEFAULT_PUBDATE
|
||||||
|
|
||||||
# handle book publisher
|
|
||||||
modif_date |= edit_book_publisher(to_save, book)
|
|
||||||
|
|
||||||
# handle book languages
|
|
||||||
modif_date |= edit_book_languages(to_save['languages'], book)
|
|
||||||
|
|
||||||
# handle book ratings
|
|
||||||
modif_date |= edit_book_ratings(to_save, book)
|
|
||||||
|
|
||||||
# handle cc data
|
|
||||||
modif_date |= edit_cc_data(book_id, book, to_save)
|
|
||||||
|
|
||||||
if modif_date:
|
if modif_date:
|
||||||
book.last_modified = datetime.utcnow()
|
book.last_modified = datetime.utcnow()
|
||||||
calibre_db.session.merge(book)
|
calibre_db.session.merge(book)
|
||||||
@@ -746,8 +820,8 @@ def edit_book(book_id):
|
|||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
return render_edit_book(book_id)
|
return render_edit_book(book_id)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
calibre_db.session.rollback()
|
calibre_db.session.rollback()
|
||||||
flash(_("Error editing book, please check logfile for details"), category="error")
|
flash(_("Error editing book, please check logfile for details"), category="error")
|
||||||
return redirect(url_for('web.show_book', book_id=book.id))
|
return redirect(url_for('web.show_book', book_id=book.id))
|
||||||
@@ -766,6 +840,7 @@ def merge_metadata(to_save, meta):
|
|||||||
to_save["description"] = to_save["description"] or Markup(
|
to_save["description"] = to_save["description"] or Markup(
|
||||||
getattr(meta, 'description', '')).unescape()
|
getattr(meta, 'description', '')).unescape()
|
||||||
|
|
||||||
|
|
||||||
def identifier_list(to_save, book):
|
def identifier_list(to_save, book):
|
||||||
"""Generate a list of Identifiers from form information"""
|
"""Generate a list of Identifiers from form information"""
|
||||||
id_type_prefix = 'identifier-type-'
|
id_type_prefix = 'identifier-type-'
|
||||||
@@ -780,43 +855,8 @@ def identifier_list(to_save, book):
|
|||||||
result.append(db.Identifiers(to_save[val_key], type_value, book.id))
|
result.append(db.Identifiers(to_save[val_key], type_value, book.id))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@editbook.route("/upload", methods=["GET", "POST"])
|
|
||||||
@login_required_if_no_ano
|
|
||||||
@upload_required
|
|
||||||
def upload():
|
|
||||||
if not config.config_uploading:
|
|
||||||
abort(404)
|
|
||||||
if request.method == 'POST' and 'btn-upload' in request.files:
|
|
||||||
for requested_file in request.files.getlist("btn-upload"):
|
|
||||||
try:
|
|
||||||
modif_date = False
|
|
||||||
# create the function for sorting...
|
|
||||||
calibre_db.update_title_sort(config)
|
|
||||||
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
|
|
||||||
|
|
||||||
# check if file extension is correct
|
|
||||||
if '.' in requested_file.filename:
|
|
||||||
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
|
||||||
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
|
|
||||||
flash(
|
|
||||||
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
|
|
||||||
ext=file_ext), category="error")
|
|
||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
|
||||||
else:
|
|
||||||
flash(_('File to be uploaded must have an extension'), category="error")
|
|
||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
|
||||||
|
|
||||||
# extract metadata from file
|
|
||||||
try:
|
|
||||||
meta = uploader.upload(requested_file, config.config_rarfile_location)
|
|
||||||
except (IOError, OSError):
|
|
||||||
log.error("File %s could not saved to temp dir", requested_file.filename)
|
|
||||||
flash(_(u"File %(filename)s could not saved to temp dir",
|
|
||||||
filename= requested_file.filename), category="error")
|
|
||||||
return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
|
||||||
title = meta.title
|
|
||||||
authr = meta.author
|
|
||||||
|
|
||||||
|
def prepare_authors_on_upload(title, authr):
|
||||||
if title != _(u'Unknown') and authr != _(u'Unknown'):
|
if title != _(u'Unknown') and authr != _(u'Unknown'):
|
||||||
entry = calibre_db.check_exists_book(authr, title)
|
entry = calibre_db.check_exists_book(authr, title)
|
||||||
if entry:
|
if entry:
|
||||||
@@ -835,7 +875,7 @@ def upload():
|
|||||||
if input_authors == ['']:
|
if input_authors == ['']:
|
||||||
input_authors = [_(u'Unknown')] # prevent empty Author
|
input_authors = [_(u'Unknown')] # prevent empty Author
|
||||||
|
|
||||||
sort_authors_list=list()
|
sort_authors_list = list()
|
||||||
db_author = None
|
db_author = None
|
||||||
for inp in input_authors:
|
for inp in input_authors:
|
||||||
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
||||||
@@ -851,12 +891,20 @@ def upload():
|
|||||||
sort_author = stored_author.sort
|
sort_author = stored_author.sort
|
||||||
sort_authors_list.append(sort_author)
|
sort_authors_list.append(sort_author)
|
||||||
sort_authors = ' & '.join(sort_authors_list)
|
sort_authors = ' & '.join(sort_authors_list)
|
||||||
|
return sort_authors, input_authors, db_author
|
||||||
|
|
||||||
|
|
||||||
|
def create_book_on_upload(modif_date, meta):
|
||||||
|
title = meta.title
|
||||||
|
authr = meta.author
|
||||||
|
sort_authors, input_authors, db_author = prepare_authors_on_upload(title, authr)
|
||||||
|
|
||||||
title_dir = helper.get_valid_filename(title)
|
title_dir = helper.get_valid_filename(title)
|
||||||
author_dir = helper.get_valid_filename(db_author.name)
|
author_dir = helper.get_valid_filename(db_author.name)
|
||||||
|
|
||||||
# combine path and normalize path from windows systems
|
# combine path and normalize path from windows systems
|
||||||
path = os.path.join(author_dir, title_dir).replace('\\', '/')
|
path = os.path.join(author_dir, title_dir).replace('\\', '/')
|
||||||
|
|
||||||
# Calibre adds books with utc as timezone
|
# Calibre adds books with utc as timezone
|
||||||
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
|
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), datetime(101, 1, 1),
|
||||||
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
|
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
|
||||||
@@ -873,6 +921,9 @@ def upload():
|
|||||||
# handle tags
|
# handle tags
|
||||||
modif_date |= edit_book_tags(meta.tags, db_book)
|
modif_date |= edit_book_tags(meta.tags, db_book)
|
||||||
|
|
||||||
|
# handle publisher
|
||||||
|
modif_date |= edit_book_publisher(meta.publisher, db_book)
|
||||||
|
|
||||||
# handle series
|
# handle series
|
||||||
modif_date |= edit_book_series(meta.series, db_book)
|
modif_date |= edit_book_series(meta.series, db_book)
|
||||||
|
|
||||||
@@ -884,19 +935,33 @@ def upload():
|
|||||||
|
|
||||||
# flush content, get db_book.id available
|
# flush content, get db_book.id available
|
||||||
calibre_db.session.flush()
|
calibre_db.session.flush()
|
||||||
|
return db_book, input_authors, title_dir
|
||||||
|
|
||||||
# Comments needs book id therfore only possible after flush
|
def file_handling_on_upload(requested_file):
|
||||||
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
|
# check if file extension is correct
|
||||||
|
if '.' in requested_file.filename:
|
||||||
|
file_ext = requested_file.filename.rsplit('.', 1)[-1].lower()
|
||||||
|
if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD:
|
||||||
|
flash(
|
||||||
|
_("File extension '%(ext)s' is not allowed to be uploaded to this server",
|
||||||
|
ext=file_ext), category="error")
|
||||||
|
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
|
else:
|
||||||
|
flash(_('File to be uploaded must have an extension'), category="error")
|
||||||
|
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
|
|
||||||
book_id = db_book.id
|
# extract metadata from file
|
||||||
title = db_book.title
|
try:
|
||||||
|
meta = uploader.upload(requested_file, config.config_rarfile_location)
|
||||||
|
except (IOError, OSError):
|
||||||
|
log.error("File %s could not saved to temp dir", requested_file.filename)
|
||||||
|
flash(_(u"File %(filename)s could not saved to temp dir",
|
||||||
|
filename=requested_file.filename), category="error")
|
||||||
|
return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json')
|
||||||
|
return meta, None
|
||||||
|
|
||||||
error = helper.update_dir_structure_file(book_id,
|
|
||||||
config.config_calibre_dir,
|
|
||||||
input_authors[0],
|
|
||||||
meta.file_path,
|
|
||||||
title_dir + meta.extension)
|
|
||||||
|
|
||||||
|
def move_coverfile(meta, db_book):
|
||||||
# move cover to final directory, including book id
|
# move cover to final directory, including book id
|
||||||
if meta.cover:
|
if meta.cover:
|
||||||
coverfile = meta.cover
|
coverfile = meta.cover
|
||||||
@@ -913,6 +978,41 @@ def upload():
|
|||||||
error=e),
|
error=e),
|
||||||
category="error")
|
category="error")
|
||||||
|
|
||||||
|
|
||||||
|
@editbook.route("/upload", methods=["GET", "POST"])
|
||||||
|
@login_required_if_no_ano
|
||||||
|
@upload_required
|
||||||
|
def upload():
|
||||||
|
if not config.config_uploading:
|
||||||
|
abort(404)
|
||||||
|
if request.method == 'POST' and 'btn-upload' in request.files:
|
||||||
|
for requested_file in request.files.getlist("btn-upload"):
|
||||||
|
try:
|
||||||
|
modif_date = False
|
||||||
|
# create the function for sorting...
|
||||||
|
calibre_db.update_title_sort(config)
|
||||||
|
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4()))
|
||||||
|
|
||||||
|
meta, error = file_handling_on_upload(requested_file)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
|
db_book, input_authors, title_dir = create_book_on_upload(modif_date, meta)
|
||||||
|
|
||||||
|
# Comments needs book id therefore only possible after flush
|
||||||
|
modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book)
|
||||||
|
|
||||||
|
book_id = db_book.id
|
||||||
|
title = db_book.title
|
||||||
|
|
||||||
|
error = helper.update_dir_structure_file(book_id,
|
||||||
|
config.config_calibre_dir,
|
||||||
|
input_authors[0],
|
||||||
|
meta.file_path,
|
||||||
|
title_dir + meta.extension)
|
||||||
|
|
||||||
|
move_coverfile(meta, db_book)
|
||||||
|
|
||||||
# save data to database, reread data
|
# save data to database, reread data
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
|
|
||||||
@@ -921,7 +1021,7 @@ def upload():
|
|||||||
if error:
|
if error:
|
||||||
flash(error, category="error")
|
flash(error, category="error")
|
||||||
uploadText=_(u"File %(file)s uploaded", file=title)
|
uploadText=_(u"File %(file)s uploaded", file=title)
|
||||||
WorkerThread.add(current_user.nickname, TaskUpload(
|
WorkerThread.add(current_user.name, TaskUpload(
|
||||||
"<a href=\"" + url_for('web.show_book', book_id=book_id) + "\">" + uploadText + "</a>"))
|
"<a href=\"" + url_for('web.show_book', book_id=book_id) + "\">" + uploadText + "</a>"))
|
||||||
|
|
||||||
if len(request.files.getlist("btn-upload")) < 2:
|
if len(request.files.getlist("btn-upload")) < 2:
|
||||||
@@ -951,7 +1051,7 @@ def convert_bookformat(book_id):
|
|||||||
|
|
||||||
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
|
log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to)
|
||||||
rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(),
|
rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(),
|
||||||
book_format_to.upper(), current_user.nickname)
|
book_format_to.upper(), current_user.name)
|
||||||
|
|
||||||
if rtn is None:
|
if rtn is None:
|
||||||
flash(_(u"Book successfully queued for converting to %(book_format)s",
|
flash(_(u"Book successfully queued for converting to %(book_format)s",
|
||||||
@@ -961,61 +1061,89 @@ def convert_bookformat(book_id):
|
|||||||
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error")
|
||||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
|
@editbook.route("/ajax/editbooks/<param>", methods=['POST'])
|
||||||
@login_required_if_no_ano
|
@login_required_if_no_ano
|
||||||
@edit_required
|
@edit_required
|
||||||
def edit_list_book(param):
|
def edit_list_book(param):
|
||||||
vals = request.form.to_dict()
|
vals = request.form.to_dict()
|
||||||
book = calibre_db.get_book(vals['pk'])
|
book = calibre_db.get_book(vals['pk'])
|
||||||
|
ret = ""
|
||||||
if param =='series_index':
|
if param =='series_index':
|
||||||
edit_book_series_index(vals['value'], book)
|
edit_book_series_index(vals['value'], book)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': book.series_index}), mimetype='application/json')
|
||||||
elif param =='tags':
|
elif param =='tags':
|
||||||
edit_book_tags(vals['value'], book)
|
edit_book_tags(vals['value'], book)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='series':
|
elif param =='series':
|
||||||
edit_book_series(vals['value'], book)
|
edit_book_series(vals['value'], book)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='publishers':
|
elif param =='publishers':
|
||||||
vals['publisher'] = vals['value']
|
edit_book_publisher(vals['value'], book)
|
||||||
edit_book_publisher(vals, book)
|
ret = Response(json.dumps({'success': True,
|
||||||
|
'newValue': ', '.join([publisher.name for publisher in book.publishers])}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='languages':
|
elif param =='languages':
|
||||||
edit_book_languages(vals['value'], book)
|
invalid = list()
|
||||||
|
edit_book_languages(vals['value'], book, invalid=invalid)
|
||||||
|
if invalid:
|
||||||
|
ret = Response(json.dumps({'success': False,
|
||||||
|
'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}),
|
||||||
|
mimetype='application/json')
|
||||||
|
else:
|
||||||
|
lang_names = list()
|
||||||
|
for lang in book.languages:
|
||||||
|
try:
|
||||||
|
lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale()))
|
||||||
|
except UnknownLocaleError:
|
||||||
|
lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name))
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='author_sort':
|
elif param =='author_sort':
|
||||||
book.author_sort = vals['value']
|
book.author_sort = vals['value']
|
||||||
elif param =='title':
|
ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}),
|
||||||
book.title = vals['value']
|
mimetype='application/json')
|
||||||
|
elif param == 'title':
|
||||||
|
sort = book.sort
|
||||||
|
handle_title_on_edit(book, vals.get('value', ""))
|
||||||
helper.update_dir_stucture(book.id, config.config_calibre_dir)
|
helper.update_dir_stucture(book.id, config.config_calibre_dir)
|
||||||
|
ret = Response(json.dumps({'success': True, 'newValue': book.title}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='sort':
|
elif param =='sort':
|
||||||
book.sort = vals['value']
|
book.sort = vals['value']
|
||||||
# ToDo: edit books
|
ret = Response(json.dumps({'success': True, 'newValue': book.sort}),
|
||||||
|
mimetype='application/json')
|
||||||
elif param =='authors':
|
elif param =='authors':
|
||||||
input_authors = vals['value'].split('&')
|
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
|
||||||
input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors))
|
|
||||||
modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author')
|
|
||||||
sort_authors_list = list()
|
|
||||||
for inp in input_authors:
|
|
||||||
stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first()
|
|
||||||
if not stored_author:
|
|
||||||
stored_author = helper.get_sorted_author(inp)
|
|
||||||
else:
|
|
||||||
stored_author = stored_author.sort
|
|
||||||
sort_authors_list.append(helper.get_sorted_author(stored_author))
|
|
||||||
sort_authors = ' & '.join(sort_authors_list)
|
|
||||||
if book.author_sort != sort_authors:
|
|
||||||
book.author_sort = sort_authors
|
|
||||||
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
|
helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0])
|
||||||
|
ret = Response(json.dumps({'success': True,
|
||||||
|
'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}),
|
||||||
|
mimetype='application/json')
|
||||||
book.last_modified = datetime.utcnow()
|
book.last_modified = datetime.utcnow()
|
||||||
calibre_db.session.commit()
|
calibre_db.session.commit()
|
||||||
return ""
|
# revert change for sort if automatic fields link is deactivated
|
||||||
|
if param == 'title' and vals.get('checkT') == "false":
|
||||||
|
book.sort = sort
|
||||||
|
calibre_db.session.commit()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
|
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
|
||||||
@login_required
|
@login_required
|
||||||
def get_sorted_entry(field, bookid):
|
def get_sorted_entry(field, bookid):
|
||||||
if field == 'title' or field == 'authors':
|
if field in ['title', 'authors', 'sort', 'author_sort']:
|
||||||
book = calibre_db.get_filtered_book(bookid)
|
book = calibre_db.get_filtered_book(bookid)
|
||||||
if book:
|
if book:
|
||||||
if field == 'title':
|
if field == 'title':
|
||||||
return json.dumps({'sort': book.sort})
|
return json.dumps({'sort': book.sort})
|
||||||
elif field == 'authors':
|
elif field == 'authors':
|
||||||
return json.dumps({'author_sort': book.author_sort})
|
return json.dumps({'author_sort': book.author_sort})
|
||||||
|
if field == 'sort':
|
||||||
|
return json.dumps({'sort': book.title})
|
||||||
|
if field == 'author_sort':
|
||||||
|
return json.dumps({'author_sort': book.author})
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -1067,6 +1195,6 @@ def merge_list_book():
|
|||||||
element.format,
|
element.format,
|
||||||
element.uncompressed_size,
|
element.uncompressed_size,
|
||||||
to_name))
|
to_name))
|
||||||
delete_book(from_book.id,"", True) # json_resp =
|
delete_book(from_book.id,"", True)
|
||||||
return json.dumps({'success': True})
|
return json.dumps({'success': True})
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -87,18 +87,29 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
lang = epub_metadata['language'].split('-', 1)[0].lower()
|
lang = epub_metadata['language'].split('-', 1)[0].lower()
|
||||||
epub_metadata['language'] = isoLanguages.get_lang3(lang)
|
epub_metadata['language'] = isoLanguages.get_lang3(lang)
|
||||||
|
|
||||||
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
|
epub_metadata = parse_epbub_series(ns, tree, epub_metadata)
|
||||||
if len(series) > 0:
|
|
||||||
epub_metadata['series'] = series[0]
|
|
||||||
else:
|
|
||||||
epub_metadata['series'] = ''
|
|
||||||
|
|
||||||
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
|
coverfile = parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path)
|
||||||
if len(series_id) > 0:
|
|
||||||
epub_metadata['series_id'] = series_id[0]
|
|
||||||
else:
|
|
||||||
epub_metadata['series_id'] = '1'
|
|
||||||
|
|
||||||
|
if not epub_metadata['title']:
|
||||||
|
title = original_file_name
|
||||||
|
else:
|
||||||
|
title = epub_metadata['title']
|
||||||
|
|
||||||
|
return BookMeta(
|
||||||
|
file_path=tmp_file_path,
|
||||||
|
extension=original_file_extension,
|
||||||
|
title=title.encode('utf-8').decode('utf-8'),
|
||||||
|
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
|
||||||
|
cover=coverfile,
|
||||||
|
description=epub_metadata['description'],
|
||||||
|
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
|
||||||
|
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
|
||||||
|
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
|
||||||
|
languages=epub_metadata['language'],
|
||||||
|
publisher="")
|
||||||
|
|
||||||
|
def parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path):
|
||||||
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
|
coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
|
||||||
coverfile = None
|
coverfile = None
|
||||||
if len(coversection) > 0:
|
if len(coversection) > 0:
|
||||||
@@ -126,20 +137,18 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
|
|||||||
coverfile = extractCover(epubZip, filename, "", tmp_file_path)
|
coverfile = extractCover(epubZip, filename, "", tmp_file_path)
|
||||||
else:
|
else:
|
||||||
coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path)
|
coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path)
|
||||||
|
return coverfile
|
||||||
|
|
||||||
if not epub_metadata['title']:
|
def parse_epbub_series(ns, tree, epub_metadata):
|
||||||
title = original_file_name
|
series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns)
|
||||||
|
if len(series) > 0:
|
||||||
|
epub_metadata['series'] = series[0]
|
||||||
else:
|
else:
|
||||||
title = epub_metadata['title']
|
epub_metadata['series'] = ''
|
||||||
|
|
||||||
return BookMeta(
|
series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns)
|
||||||
file_path=tmp_file_path,
|
if len(series_id) > 0:
|
||||||
extension=original_file_extension,
|
epub_metadata['series_id'] = series_id[0]
|
||||||
title=title.encode('utf-8').decode('utf-8'),
|
else:
|
||||||
author=epub_metadata['creator'].encode('utf-8').decode('utf-8'),
|
epub_metadata['series_id'] = '1'
|
||||||
cover=coverfile,
|
return epub_metadata
|
||||||
description=epub_metadata['description'],
|
|
||||||
tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'),
|
|
||||||
series=epub_metadata['series'].encode('utf-8').decode('utf-8'),
|
|
||||||
series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'),
|
|
||||||
languages=epub_metadata['language'])
|
|
||||||
|
|||||||
@@ -60,14 +60,8 @@ def init_errorhandler():
|
|||||||
if services.ldap:
|
if services.ldap:
|
||||||
# Only way of catching the LDAPException upon logging in with LDAP server down
|
# Only way of catching the LDAPException upon logging in with LDAP server down
|
||||||
@app.errorhandler(services.ldap.LDAPException)
|
@app.errorhandler(services.ldap.LDAPException)
|
||||||
|
# pylint: disable=unused-variable
|
||||||
def handle_exception(e):
|
def handle_exception(e):
|
||||||
log.debug('LDAP server not accessible while trying to login to opds feed')
|
log.debug('LDAP server not accessible while trying to login to opds feed')
|
||||||
return error_http(FailedDependency())
|
return error_http(FailedDependency())
|
||||||
|
|
||||||
|
|
||||||
# @app.errorhandler(InvalidRequestError)
|
|
||||||
#@app.errorhandler(OperationalError)
|
|
||||||
#def handle_db_exception(e):
|
|
||||||
# db.session.rollback()
|
|
||||||
# log.error('Database request error: %s',e)
|
|
||||||
# return internal_error(InternalServerError(e))
|
|
||||||
|
|||||||
@@ -29,52 +29,53 @@ def get_fb2_info(tmp_file_path, original_file_extension):
|
|||||||
'l': 'http://www.w3.org/1999/xlink',
|
'l': 'http://www.w3.org/1999/xlink',
|
||||||
}
|
}
|
||||||
|
|
||||||
fb2_file = open(tmp_file_path)
|
fb2_file = open(tmp_file_path, encoding="utf-8")
|
||||||
tree = etree.fromstring(fb2_file.read())
|
tree = etree.fromstring(fb2_file.read().encode())
|
||||||
|
|
||||||
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
|
authors = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:author', namespaces=ns)
|
||||||
|
|
||||||
def get_author(element):
|
def get_author(element):
|
||||||
last_name = element.xpath('fb:last-name/text()', namespaces=ns)
|
last_name = element.xpath('fb:last-name/text()', namespaces=ns)
|
||||||
if len(last_name):
|
if len(last_name):
|
||||||
last_name = last_name[0].encode('utf-8')
|
last_name = last_name[0]
|
||||||
else:
|
else:
|
||||||
last_name = u''
|
last_name = u''
|
||||||
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
|
middle_name = element.xpath('fb:middle-name/text()', namespaces=ns)
|
||||||
if len(middle_name):
|
if len(middle_name):
|
||||||
middle_name = middle_name[0].encode('utf-8')
|
middle_name = middle_name[0]
|
||||||
else:
|
else:
|
||||||
middle_name = u''
|
middle_name = u''
|
||||||
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
|
first_name = element.xpath('fb:first-name/text()', namespaces=ns)
|
||||||
if len(first_name):
|
if len(first_name):
|
||||||
first_name = first_name[0].encode('utf-8')
|
first_name = first_name[0]
|
||||||
else:
|
else:
|
||||||
first_name = u''
|
first_name = u''
|
||||||
return (first_name.decode('utf-8') + u' '
|
return (first_name + u' '
|
||||||
+ middle_name.decode('utf-8') + u' '
|
+ middle_name + u' '
|
||||||
+ last_name.decode('utf-8')).encode('utf-8')
|
+ last_name)
|
||||||
|
|
||||||
author = str(", ".join(map(get_author, authors)))
|
author = str(", ".join(map(get_author, authors)))
|
||||||
|
|
||||||
title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns)
|
title = tree.xpath('/fb:FictionBook/fb:description/fb:title-info/fb:book-title/text()', namespaces=ns)
|
||||||
if len(title):
|
if len(title):
|
||||||
title = str(title[0].encode('utf-8'))
|
title = str(title[0])
|
||||||
else:
|
else:
|
||||||
title = u''
|
title = u''
|
||||||
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
|
description = tree.xpath('/fb:FictionBook/fb:description/fb:publish-info/fb:book-name/text()', namespaces=ns)
|
||||||
if len(description):
|
if len(description):
|
||||||
description = str(description[0].encode('utf-8'))
|
description = str(description[0])
|
||||||
else:
|
else:
|
||||||
description = u''
|
description = u''
|
||||||
|
|
||||||
return BookMeta(
|
return BookMeta(
|
||||||
file_path=tmp_file_path,
|
file_path=tmp_file_path,
|
||||||
extension=original_file_extension,
|
extension=original_file_extension,
|
||||||
title=title.decode('utf-8'),
|
title=title,
|
||||||
author=author.decode('utf-8'),
|
author=author,
|
||||||
cover=None,
|
cover=None,
|
||||||
description=description.decode('utf-8'),
|
description=description,
|
||||||
tags="",
|
tags="",
|
||||||
series="",
|
series="",
|
||||||
series_id="",
|
series_id="",
|
||||||
languages="")
|
languages="",
|
||||||
|
publisher="")
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ except ImportError as 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' #nosec
|
||||||
|
|
||||||
|
|
||||||
@gdrive.route("/authenticate")
|
@gdrive.route("/authenticate")
|
||||||
@@ -142,7 +142,7 @@ def on_received_watch_confirmation():
|
|||||||
else:
|
else:
|
||||||
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
dbpath = os.path.join(config.config_calibre_dir, "metadata.db").encode()
|
||||||
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
if not response['deleted'] and response['file']['title'] == 'metadata.db' \
|
||||||
and response['file']['md5Checksum'] != hashlib.md5(dbpath):
|
and response['file']['md5Checksum'] != hashlib.md5(dbpath): # nosec
|
||||||
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
tmp_dir = os.path.join(tempfile.gettempdir(), 'calibre_web')
|
||||||
if not os.path.isdir(tmp_dir):
|
if not os.path.isdir(tmp_dir):
|
||||||
os.mkdir(tmp_dir)
|
os.mkdir(tmp_dir)
|
||||||
@@ -155,6 +155,6 @@ def on_received_watch_confirmation():
|
|||||||
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
# prevent error on windows, as os.rename does on existing files, also allow cross hdd move
|
||||||
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath)
|
||||||
calibre_db.reconnect_db(config, ub.app_DB_path)
|
calibre_db.reconnect_db(config, ub.app_DB_path)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ from sqlalchemy import create_engine
|
|||||||
from sqlalchemy import Column, UniqueConstraint
|
from sqlalchemy import Column, UniqueConstraint
|
||||||
from sqlalchemy import String, Integer
|
from sqlalchemy import String, Integer
|
||||||
from sqlalchemy.orm import sessionmaker, scoped_session
|
from sqlalchemy.orm import sessionmaker, scoped_session
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
try:
|
||||||
|
# Compatibility with sqlalchemy 2.0
|
||||||
|
from sqlalchemy.orm import declarative_base
|
||||||
|
except ImportError:
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -198,8 +202,8 @@ def getDrive(drive=None, gauth=None):
|
|||||||
gauth.Refresh()
|
gauth.Refresh()
|
||||||
except RefreshError as e:
|
except RefreshError as e:
|
||||||
log.error("Google Drive error: %s", e)
|
log.error("Google Drive error: %s", e)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
else:
|
else:
|
||||||
# Initialize the saved creds
|
# Initialize the saved creds
|
||||||
gauth.Authorize()
|
gauth.Authorize()
|
||||||
@@ -217,7 +221,7 @@ def listRootFolders():
|
|||||||
drive = getDrive(Gdrive.Instance().drive)
|
drive = getDrive(Gdrive.Instance().drive)
|
||||||
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
|
folder = "'root' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed = false"
|
||||||
fileList = drive.ListFile({'q': folder}).GetList()
|
fileList = drive.ListFile({'q': folder}).GetList()
|
||||||
except (ServerNotFoundError, ssl.SSLError) as e:
|
except (ServerNotFoundError, ssl.SSLError, RefreshError) as e:
|
||||||
log.info("GDrive Error %s" % e)
|
log.info("GDrive Error %s" % e)
|
||||||
fileList = []
|
fileList = []
|
||||||
return fileList
|
return fileList
|
||||||
@@ -253,7 +257,12 @@ def getEbooksFolderId(drive=None):
|
|||||||
log.error('Error gDrive, root ID not found')
|
log.error('Error gDrive, root ID not found')
|
||||||
gDriveId.path = '/'
|
gDriveId.path = '/'
|
||||||
session.merge(gDriveId)
|
session.merge(gDriveId)
|
||||||
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
return gDriveId.gdrive_id
|
return gDriveId.gdrive_id
|
||||||
|
|
||||||
|
|
||||||
@@ -268,6 +277,7 @@ def getFile(pathId, fileName, drive):
|
|||||||
|
|
||||||
def getFolderId(path, drive):
|
def getFolderId(path, drive):
|
||||||
# drive = getDrive(drive)
|
# drive = getDrive(drive)
|
||||||
|
try:
|
||||||
currentFolderId = getEbooksFolderId(drive)
|
currentFolderId = getEbooksFolderId(drive)
|
||||||
sqlCheckPath = path if path[-1] == '/' else path + '/'
|
sqlCheckPath = path if path[-1] == '/' else path + '/'
|
||||||
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
|
storedPathName = session.query(GdriveId).filter(GdriveId.path == sqlCheckPath).first()
|
||||||
@@ -299,6 +309,10 @@ def getFolderId(path, drive):
|
|||||||
session.commit()
|
session.commit()
|
||||||
else:
|
else:
|
||||||
currentFolderId = storedPathName.gdrive_id
|
currentFolderId = storedPathName.gdrive_id
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
return currentFolderId
|
return currentFolderId
|
||||||
|
|
||||||
|
|
||||||
@@ -342,7 +356,7 @@ def moveGdriveFolderRemote(origin_file, target_folder):
|
|||||||
addParents=gFileTargetDir['id'],
|
addParents=gFileTargetDir['id'],
|
||||||
removeParents=previous_parents,
|
removeParents=previous_parents,
|
||||||
fields='id, parents').execute()
|
fields='id, parents').execute()
|
||||||
# if previous_parents has no childs anymore, delete original fileparent
|
# if previous_parents has no children anymore, delete original fileparent
|
||||||
if len(children['items']) == 1:
|
if len(children['items']) == 1:
|
||||||
deleteDatabaseEntry(previous_parents)
|
deleteDatabaseEntry(previous_parents)
|
||||||
drive.auth.service.files().delete(fileId=previous_parents).execute()
|
drive.auth.service.files().delete(fileId=previous_parents).execute()
|
||||||
@@ -493,8 +507,8 @@ def getChangeById (drive, change_id):
|
|||||||
except (errors.HttpError) as error:
|
except (errors.HttpError) as error:
|
||||||
log.error(error)
|
log.error(error)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.error(e)
|
log.error(ex)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -503,9 +517,10 @@ def deleteDatabaseOnChange():
|
|||||||
try:
|
try:
|
||||||
session.query(GdriveId).delete()
|
session.query(GdriveId).delete()
|
||||||
session.commit()
|
session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError) as ex:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
log.info(u"GDrive DB is not Writeable")
|
log.debug('Database error: %s', ex)
|
||||||
|
log.error(u"GDrive DB is not Writeable")
|
||||||
|
|
||||||
|
|
||||||
def updateGdriveCalibreFromLocal():
|
def updateGdriveCalibreFromLocal():
|
||||||
@@ -520,13 +535,23 @@ def updateDatabaseOnEdit(ID,newPath):
|
|||||||
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
|
storedPathName = session.query(GdriveId).filter(GdriveId.gdrive_id == ID).first()
|
||||||
if storedPathName:
|
if storedPathName:
|
||||||
storedPathName.path = sqlCheckPath
|
storedPathName.path = sqlCheckPath
|
||||||
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
# Deletes the hashes in database of deleted book
|
# Deletes the hashes in database of deleted book
|
||||||
def deleteDatabaseEntry(ID):
|
def deleteDatabaseEntry(ID):
|
||||||
session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete()
|
session.query(GdriveId).filter(GdriveId.gdrive_id == ID).delete()
|
||||||
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
|
||||||
# Gets cover file from gdrive
|
# Gets cover file from gdrive
|
||||||
@@ -543,7 +568,12 @@ def get_cover_via_gdrive(cover_path):
|
|||||||
permissionAdded = PermissionAdded()
|
permissionAdded = PermissionAdded()
|
||||||
permissionAdded.gdrive_id = df['id']
|
permissionAdded.gdrive_id = df['id']
|
||||||
session.add(permissionAdded)
|
session.add(permissionAdded)
|
||||||
|
try:
|
||||||
session.commit()
|
session.commit()
|
||||||
|
except OperationalError as ex:
|
||||||
|
log.error("gdrive.db DB is not Writeable")
|
||||||
|
log.debug('Database error: %s', ex)
|
||||||
|
session.rollback()
|
||||||
return df.metadata.get('webContentLink')
|
return df.metadata.get('webContentLink')
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from babel.units import format_unit
|
|||||||
from flask import send_from_directory, make_response, redirect, abort, url_for
|
from flask import send_from_directory, make_response, redirect, abort, url_for
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from sqlalchemy.sql.expression import true, false, and_, text
|
from sqlalchemy.sql.expression import true, false, and_, text, func
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ try:
|
|||||||
except (ImportError, RuntimeError) as e:
|
except (ImportError, RuntimeError) as e:
|
||||||
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
|
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
|
||||||
use_IM = False
|
use_IM = False
|
||||||
|
MissingDelegateError = BaseException
|
||||||
|
|
||||||
|
|
||||||
# Convert existing book entry to new format
|
# Convert existing book entry to new format
|
||||||
@@ -133,53 +134,14 @@ 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(entry):
|
def check_send_to_kindle_with_converter(formats):
|
||||||
"""
|
|
||||||
returns all available book formats for sending to Kindle
|
|
||||||
"""
|
|
||||||
if len(entry.data):
|
|
||||||
bookformats = list()
|
bookformats = list()
|
||||||
if not config.config_converterpath:
|
|
||||||
# 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')})
|
|
||||||
else:
|
|
||||||
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 config.config_converterpath:
|
|
||||||
if 'EPUB' in formats and 'MOBI' not in formats:
|
if 'EPUB' in formats and 'MOBI' not in formats:
|
||||||
bookformats.append({'format': 'Mobi',
|
bookformats.append({'format': 'Mobi',
|
||||||
'convert':1,
|
'convert': 1,
|
||||||
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
|
'text': _('Convert %(orig)s to %(format)s and send to Kindle',
|
||||||
orig='Epub',
|
orig='Epub',
|
||||||
format='Mobi')})
|
format='Mobi')})
|
||||||
@@ -190,6 +152,33 @@ def check_send_to_kindle(entry):
|
|||||||
orig='Azw3',
|
orig='Azw3',
|
||||||
format='Mobi')})
|
format='Mobi')})
|
||||||
return bookformats
|
return bookformats
|
||||||
|
|
||||||
|
|
||||||
|
def check_send_to_kindle(entry):
|
||||||
|
"""
|
||||||
|
returns all available book formats for sending to Kindle
|
||||||
|
"""
|
||||||
|
formats = list()
|
||||||
|
bookformats = list()
|
||||||
|
if len(entry.data):
|
||||||
|
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 'PDF' in formats:
|
||||||
|
bookformats.append({'format': 'Pdf',
|
||||||
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Pdf')})
|
||||||
|
if 'AZW' in formats:
|
||||||
|
bookformats.append({'format': 'Azw',
|
||||||
|
'convert': 0,
|
||||||
|
'text': _('Send %(format)s to Kindle', format='Azw')})
|
||||||
|
if config.config_converterpath:
|
||||||
|
bookformats.extend(check_send_to_kindle_with_converter(formats))
|
||||||
|
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
|
||||||
@@ -198,7 +187,7 @@ def check_send_to_kindle(entry):
|
|||||||
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
|
# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return
|
||||||
# list with supported formats
|
# list with supported formats
|
||||||
def check_read_formats(entry):
|
def check_read_formats(entry):
|
||||||
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR'}
|
EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'CBZ', 'CBT', 'CBR', 'DJVU'}
|
||||||
bookformats = list()
|
bookformats = list()
|
||||||
if len(entry.data):
|
if len(entry.data):
|
||||||
for ele in iter(entry.data):
|
for ele in iter(entry.data):
|
||||||
@@ -491,8 +480,8 @@ def reset_password(user_id):
|
|||||||
password = generate_random_password()
|
password = generate_random_password()
|
||||||
existing_user.password = generate_password_hash(password)
|
existing_user.password = generate_password_hash(password)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
send_registration_mail(existing_user.email, existing_user.nickname, password, True)
|
send_registration_mail(existing_user.email, existing_user.name, password, True)
|
||||||
return 1, existing_user.nickname
|
return 1, existing_user.name
|
||||||
except Exception:
|
except Exception:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
return 0, None
|
return 0, None
|
||||||
@@ -509,11 +498,37 @@ def generate_random_password():
|
|||||||
|
|
||||||
def uniq(inpt):
|
def uniq(inpt):
|
||||||
output = []
|
output = []
|
||||||
|
inpt = [ " ".join(inp.split()) for inp in inpt]
|
||||||
for x in inpt:
|
for x in inpt:
|
||||||
if x not in output:
|
if x not in output:
|
||||||
output.append(x)
|
output.append(x)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def check_email(email):
|
||||||
|
email = valid_email(email)
|
||||||
|
if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first():
|
||||||
|
log.error(u"Found an existing account for this e-mail address")
|
||||||
|
raise Exception(_(u"Found an existing account for this e-mail address"))
|
||||||
|
return email
|
||||||
|
|
||||||
|
|
||||||
|
def check_username(username):
|
||||||
|
username = username.strip()
|
||||||
|
if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar():
|
||||||
|
log.error(u"This username is already taken")
|
||||||
|
raise Exception (_(u"This username is already taken"))
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
def valid_email(email):
|
||||||
|
email = email.strip()
|
||||||
|
# Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation
|
||||||
|
if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$",
|
||||||
|
email):
|
||||||
|
log.error(u"Invalid e-mail address format")
|
||||||
|
raise Exception(_(u"Invalid e-mail address format"))
|
||||||
|
return email
|
||||||
|
|
||||||
# ################################# External interface #################################
|
# ################################# External interface #################################
|
||||||
|
|
||||||
|
|
||||||
@@ -561,8 +576,8 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
|
|||||||
else:
|
else:
|
||||||
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
log.error('%s/cover.jpg not found on Google Drive', book.path)
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
return get_cover_on_failure(use_generic_cover_on_failure)
|
return get_cover_on_failure(use_generic_cover_on_failure)
|
||||||
else:
|
else:
|
||||||
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
cover_file_path = os.path.join(config.config_calibre_dir, book.path)
|
||||||
@@ -591,10 +606,6 @@ def save_cover_from_url(url, book_path):
|
|||||||
|
|
||||||
|
|
||||||
def save_cover_from_filestorage(filepath, saved_filename, img):
|
def save_cover_from_filestorage(filepath, saved_filename, img):
|
||||||
if hasattr(img,"metadata"):
|
|
||||||
img.save(filename=os.path.join(filepath, saved_filename))
|
|
||||||
img.close()
|
|
||||||
else:
|
|
||||||
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
|
# check if file path exists, otherwise create it, copy file to calibre path and delete temp file
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
try:
|
try:
|
||||||
@@ -603,6 +614,17 @@ def save_cover_from_filestorage(filepath, saved_filename, img):
|
|||||||
log.error(u"Failed to create path for cover")
|
log.error(u"Failed to create path for cover")
|
||||||
return False, _(u"Failed to create path for cover")
|
return False, _(u"Failed to create path for cover")
|
||||||
try:
|
try:
|
||||||
|
# upload of jgp file without wand
|
||||||
|
if isinstance(img, requests.Response):
|
||||||
|
with open(os.path.join(filepath, saved_filename), 'wb') as f:
|
||||||
|
f.write(img.content)
|
||||||
|
else:
|
||||||
|
if hasattr(img, "metadata"):
|
||||||
|
# upload of jpg/png... via url
|
||||||
|
img.save(filename=os.path.join(filepath, saved_filename))
|
||||||
|
img.close()
|
||||||
|
else:
|
||||||
|
# upload of jpg/png... from hdd
|
||||||
img.save(os.path.join(filepath, saved_filename))
|
img.save(os.path.join(filepath, saved_filename))
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
log.error(u"Cover-file is not a valid image file, or could not be stored")
|
log.error(u"Cover-file is not a valid image file, or could not be stored")
|
||||||
@@ -671,6 +693,7 @@ def do_download_file(book, book_format, client, data, headers):
|
|||||||
# ToDo Check headers parameter
|
# ToDo Check headers parameter
|
||||||
for element in headers:
|
for element in headers:
|
||||||
response.headers[element[0]] = element[1]
|
response.headers[element[0]] = element[1]
|
||||||
|
log.info('Downloading file: {}'.format(os.path.join(filename, data.name + "." + book_format)))
|
||||||
return response
|
return response
|
||||||
|
|
||||||
##################################
|
##################################
|
||||||
@@ -710,7 +733,6 @@ def json_serial(obj):
|
|||||||
'seconds': obj.seconds,
|
'seconds': obj.seconds,
|
||||||
'microseconds': obj.microseconds,
|
'microseconds': obj.microseconds,
|
||||||
}
|
}
|
||||||
# return obj.isoformat()
|
|
||||||
raise TypeError("Type %s not serializable" % type(obj))
|
raise TypeError("Type %s not serializable" % type(obj))
|
||||||
|
|
||||||
|
|
||||||
@@ -734,8 +756,8 @@ 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, __, task in tasklist:
|
||||||
if user == current_user.nickname or current_user.role_admin():
|
if user == current_user.name or current_user.role_admin():
|
||||||
ret = {}
|
ret = {}
|
||||||
if task.start_time:
|
if task.start_time:
|
||||||
ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale())
|
ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale())
|
||||||
@@ -773,8 +795,8 @@ def tags_filters():
|
|||||||
# checks if domain is in database (including wildcards)
|
# checks if domain is in database (including wildcards)
|
||||||
# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name;
|
# example SELECT * FROM @TABLE WHERE 'abcdefg' LIKE Name;
|
||||||
# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/
|
# from https://code.luasoftware.com/tutorials/flask/execute-raw-sql-in-flask-sqlalchemy/
|
||||||
|
# in all calls the email address is checked for validity
|
||||||
def check_valid_domain(domain_text):
|
def check_valid_domain(domain_text):
|
||||||
# domain_text = domain_text.split('@', 1)[-1].lower()
|
|
||||||
sql = "SELECT * FROM registration WHERE (:domain LIKE domain and allow = 1);"
|
sql = "SELECT * FROM registration WHERE (:domain LIKE domain and allow = 1);"
|
||||||
result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all()
|
result = ub.session.query(ub.Registration).from_statement(text(sql)).params(domain=domain_text).all()
|
||||||
if not len(result):
|
if not len(result):
|
||||||
@@ -808,6 +830,7 @@ def get_download_link(book_id, book_format, client):
|
|||||||
if book:
|
if book:
|
||||||
data1 = calibre_db.get_book_format(book.id, book_format.upper())
|
data1 = calibre_db.get_book_format(book.id, book_format.upper())
|
||||||
else:
|
else:
|
||||||
|
log.error("Book id {} not found for downloading".format(book_id))
|
||||||
abort(404)
|
abort(404)
|
||||||
if data1:
|
if data1:
|
||||||
# collect downloaded books only for registered user and not for anonymous user
|
# collect downloaded books only for registered user and not for anonymous user
|
||||||
|
|||||||
@@ -57,27 +57,30 @@ def get_language_name(locale, lang_code):
|
|||||||
|
|
||||||
def get_language_codes(locale, language_names, remainder=None):
|
def get_language_codes(locale, language_names, remainder=None):
|
||||||
language_names = set(x.strip().lower() for x in language_names if x)
|
language_names = set(x.strip().lower() for x in language_names if x)
|
||||||
languages = list()
|
lang = list()
|
||||||
for k, v in get_language_names(locale).items():
|
for k, v in get_language_names(locale).items():
|
||||||
v = v.lower()
|
v = v.lower()
|
||||||
if v in language_names:
|
if v in language_names:
|
||||||
languages.append(k)
|
lang.append(k)
|
||||||
language_names.remove(v)
|
language_names.remove(v)
|
||||||
if remainder is not None:
|
if remainder is not None and language_names:
|
||||||
remainder.extend(language_names)
|
remainder.extend(language_names)
|
||||||
return languages
|
return lang
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_valid_language_codes(locale, language_names, remainder=None):
|
def get_valid_language_codes(locale, language_names, remainder=None):
|
||||||
languages = list()
|
lang = 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)
|
lang.append(k)
|
||||||
language_names.remove(k)
|
language_names.remove(k)
|
||||||
if remainder is not None and len(language_names):
|
if remainder is not None and len(language_names):
|
||||||
remainder.extend(language_names)
|
remainder.extend(language_names)
|
||||||
return languages
|
return lang
|
||||||
|
|
||||||
|
|
||||||
def get_lang3(lang):
|
def get_lang3(lang):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ def formatdate_filter(val):
|
|||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
|
log.error('Babel error: %s, Current user locale: %s, Current User: %s', e,
|
||||||
current_user.locale,
|
current_user.locale,
|
||||||
current_user.nickname
|
current_user.name
|
||||||
)
|
)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@@ -122,10 +122,13 @@ def formatfloat(value, decimals=1):
|
|||||||
@jinjia.app_template_filter('formatseriesindex')
|
@jinjia.app_template_filter('formatseriesindex')
|
||||||
def formatseriesindex_filter(series_index):
|
def formatseriesindex_filter(series_index):
|
||||||
if series_index:
|
if series_index:
|
||||||
|
try:
|
||||||
if int(series_index) - series_index == 0:
|
if int(series_index) - series_index == 0:
|
||||||
return int(series_index)
|
return int(series_index)
|
||||||
else:
|
else:
|
||||||
return series_index
|
return series_index
|
||||||
|
except ValueError:
|
||||||
|
return series_index
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@jinjia.app_template_filter('uuidfilter')
|
@jinjia.app_template_filter('uuidfilter')
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ from flask import (
|
|||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from werkzeug.datastructures import Headers
|
from werkzeug.datastructures import Headers
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_
|
||||||
from sqlalchemy.orm import load_only
|
|
||||||
from sqlalchemy.exc import StatementError
|
from sqlalchemy.exc import StatementError
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -159,6 +158,7 @@ def HandleSyncRequest():
|
|||||||
.filter(db.Books.last_modified >= sync_token.books_last_modified)
|
.filter(db.Books.last_modified >= sync_token.books_last_modified)
|
||||||
.filter(db.Books.id>sync_token.books_last_id)
|
.filter(db.Books.id>sync_token.books_last_id)
|
||||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
|
.filter(calibre_db.common_filters())
|
||||||
.order_by(db.Books.last_modified)
|
.order_by(db.Books.last_modified)
|
||||||
.order_by(db.Books.id)
|
.order_by(db.Books.id)
|
||||||
.limit(SYNC_ITEM_LIMIT)
|
.limit(SYNC_ITEM_LIMIT)
|
||||||
@@ -169,6 +169,7 @@ def HandleSyncRequest():
|
|||||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||||
.filter(db.Books.last_modified > sync_token.books_last_modified)
|
.filter(db.Books.last_modified > sync_token.books_last_modified)
|
||||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||||
|
.filter(calibre_db.common_filters())
|
||||||
.order_by(db.Books.last_modified)
|
.order_by(db.Books.last_modified)
|
||||||
.order_by(db.Books.id)
|
.order_by(db.Books.id)
|
||||||
.limit(SYNC_ITEM_LIMIT)
|
.limit(SYNC_ITEM_LIMIT)
|
||||||
@@ -176,6 +177,10 @@ def HandleSyncRequest():
|
|||||||
|
|
||||||
reading_states_in_new_entitlements = []
|
reading_states_in_new_entitlements = []
|
||||||
for book in changed_entries:
|
for book in changed_entries:
|
||||||
|
formats = [data.format for data in book.Books.data]
|
||||||
|
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||||
|
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||||
|
|
||||||
kobo_reading_state = get_or_create_reading_state(book.Books.id)
|
kobo_reading_state = get_or_create_reading_state(book.Books.id)
|
||||||
entitlement = {
|
entitlement = {
|
||||||
"BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)),
|
"BookEntitlement": create_book_entitlement(book.Books, archived=(book.is_archived == True)),
|
||||||
@@ -259,8 +264,8 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
|||||||
extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode")
|
extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode")
|
||||||
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
|
extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e))
|
log.error("Failed to receive or parse response from Kobo's sync endpoint: {}".format(ex))
|
||||||
if set_cont:
|
if set_cont:
|
||||||
extra_headers["x-kobo-sync"] = "continue"
|
extra_headers["x-kobo-sync"] = "continue"
|
||||||
sync_token.to_headers(extra_headers)
|
sync_token.to_headers(extra_headers)
|
||||||
@@ -302,7 +307,8 @@ def get_download_url_for_book(book, book_format):
|
|||||||
book_format=book_format.lower()
|
book_format=book_format.lower()
|
||||||
)
|
)
|
||||||
return url_for(
|
return url_for(
|
||||||
"web.download_link",
|
"kobo.download_book",
|
||||||
|
auth_token=kobo_auth.get_auth_token(),
|
||||||
book_id=book.id,
|
book_id=book.id,
|
||||||
book_format=book_format.lower(),
|
book_format=book_format.lower(),
|
||||||
_external=True,
|
_external=True,
|
||||||
@@ -889,17 +895,6 @@ def HandleProductsRequest(dummy=None):
|
|||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
'''@kobo.errorhandler(404)
|
|
||||||
def handle_404(err):
|
|
||||||
# This handler acts as a catch-all for endpoints that we don't have an interest in
|
|
||||||
# implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc)
|
|
||||||
if err:
|
|
||||||
print('404')
|
|
||||||
return jsonify(error=str(err)), 404
|
|
||||||
log.debug("Unknown Request received: %s, method: %s, data: %s", request.base_url, request.method, request.data)
|
|
||||||
return redirect_or_proxy_request()'''
|
|
||||||
|
|
||||||
|
|
||||||
def make_calibre_web_auth_response():
|
def make_calibre_web_auth_response():
|
||||||
# As described in kobo_auth.py, CalibreWeb doesn't make use practical use of this auth/device API call for
|
# As described in kobo_auth.py, CalibreWeb doesn't make use practical use of this auth/device API call for
|
||||||
# authentation (nor for authorization). We return a dummy response just to keep the device happy.
|
# authentation (nor for authorization). We return a dummy response just to keep the device happy.
|
||||||
@@ -926,7 +921,7 @@ def HandleAuthRequest():
|
|||||||
if config.config_kobo_proxy:
|
if config.config_kobo_proxy:
|
||||||
try:
|
try:
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
except:
|
except Exception:
|
||||||
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
|
log.error("Failed to receive or parse response from Kobo's auth endpoint. Falling back to un-proxied mode.")
|
||||||
return make_calibre_web_auth_response()
|
return make_calibre_web_auth_response()
|
||||||
|
|
||||||
@@ -943,7 +938,7 @@ def HandleInitRequest():
|
|||||||
store_response_json = store_response.json()
|
store_response_json = store_response.json()
|
||||||
if "Resources" in store_response_json:
|
if "Resources" in store_response_json:
|
||||||
kobo_resources = store_response_json["Resources"]
|
kobo_resources = store_response_json["Resources"]
|
||||||
except:
|
except Exception:
|
||||||
log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.")
|
log.error("Failed to receive or parse response from Kobo's init endpoint. Falling back to un-proxied mode.")
|
||||||
if not kobo_resources:
|
if not kobo_resources:
|
||||||
kobo_resources = NATIVE_KOBO_RESOURCES()
|
kobo_resources = NATIVE_KOBO_RESOURCES()
|
||||||
@@ -1004,7 +999,6 @@ def HandleInitRequest():
|
|||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
@download_required
|
@download_required
|
||||||
def download_book(book_id, book_format):
|
def download_book(book_id, book_format):
|
||||||
|
|
||||||
return get_download_link(book_id, book_format, "kobo")
|
return get_download_link(book_id, book_format, "kobo")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ from datetime import datetime
|
|||||||
from os import urandom
|
from os import urandom
|
||||||
|
|
||||||
from flask import g, Blueprint, url_for, abort, request
|
from flask import g, Blueprint, url_for, abort, request
|
||||||
from flask_login import login_user, login_required
|
from flask_login import login_user, current_user, login_required
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
from . import logger, ub, lm
|
from . import logger, config, calibre_db, db, helper, ub, lm
|
||||||
from .render_template import render_title_template
|
from .render_template import render_title_template
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -81,6 +81,7 @@ log = logger.create()
|
|||||||
|
|
||||||
def register_url_value_preprocessor(kobo):
|
def register_url_value_preprocessor(kobo):
|
||||||
@kobo.url_value_preprocessor
|
@kobo.url_value_preprocessor
|
||||||
|
# pylint: disable=unused-variable
|
||||||
def pop_auth_token(__, values):
|
def pop_auth_token(__, values):
|
||||||
g.auth_token = values.pop("auth_token")
|
g.auth_token = values.pop("auth_token")
|
||||||
|
|
||||||
@@ -148,6 +149,14 @@ def generate_auth_token(user_id):
|
|||||||
|
|
||||||
ub.session.add(auth_token)
|
ub.session.add(auth_token)
|
||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
|
|
||||||
|
books = calibre_db.session.query(db.Books).join(db.Data).all()
|
||||||
|
|
||||||
|
for book in books:
|
||||||
|
formats = [data.format for data in book.data]
|
||||||
|
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||||
|
helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||||
|
|
||||||
return render_title_template(
|
return render_title_template(
|
||||||
"generate_kobo_auth_url.html",
|
"generate_kobo_auth_url.html",
|
||||||
title=_(u"Kobo Setup"),
|
title=_(u"Kobo Setup"),
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ class _Logger(logging.Logger):
|
|||||||
|
|
||||||
|
|
||||||
def debug_no_auth(self, message, *args, **kwargs):
|
def debug_no_auth(self, message, *args, **kwargs):
|
||||||
|
message = message.strip("\r\n")
|
||||||
if message.startswith("send: AUTH"):
|
if message.startswith("send: AUTH"):
|
||||||
self.debug(message[:16], stacklevel=2, *args, **kwargs)
|
self.debug(message[:16], *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
self.debug(message, stacklevel=2, *args, **kwargs)
|
self.debug(message, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get(name=None):
|
def get(name=None):
|
||||||
@@ -153,11 +153,11 @@ def setup(log_file, log_level=None):
|
|||||||
file_handler.baseFilename = log_file
|
file_handler.baseFilename = log_file
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
file_handler = RotatingFileHandler(log_file, maxBytes=50000, backupCount=2, encoding='utf-8')
|
file_handler = RotatingFileHandler(log_file, maxBytes=100000, backupCount=2, encoding='utf-8')
|
||||||
except IOError:
|
except IOError:
|
||||||
if log_file == DEFAULT_LOG_FILE:
|
if log_file == DEFAULT_LOG_FILE:
|
||||||
raise
|
raise
|
||||||
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=50000, backupCount=2, encoding='utf-8')
|
file_handler = RotatingFileHandler(DEFAULT_LOG_FILE, maxBytes=100000, backupCount=2, encoding='utf-8')
|
||||||
log_file = ""
|
log_file = ""
|
||||||
file_handler.setFormatter(FORMATTER)
|
file_handler.setFormatter(FORMATTER)
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 +33,8 @@ except ImportError:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
|
||||||
class OAuthBackend(SQLAlchemyBackend):
|
class OAuthBackend(SQLAlchemyBackend):
|
||||||
"""
|
"""
|
||||||
Stores and retrieves OAuth tokens using a relational database through
|
Stores and retrieves OAuth tokens using a relational database through
|
||||||
the `SQLAlchemy`_ ORM.
|
the `SQLAlchemy`_ ORM.
|
||||||
@@ -162,6 +161,3 @@ try:
|
|||||||
self.cache.delete(self.make_cache_key(
|
self.cache.delete(self.make_cache_key(
|
||||||
blueprint=blueprint, user=user, user_id=user_id,
|
blueprint=blueprint, user=user, user_id=user_id,
|
||||||
))
|
))
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -30,15 +30,20 @@ from flask_babel import gettext as _
|
|||||||
from flask_dance.consumer import oauth_authorized, oauth_error
|
from flask_dance.consumer import oauth_authorized, oauth_error
|
||||||
from flask_dance.contrib.github import make_github_blueprint, github
|
from flask_dance.contrib.github import make_github_blueprint, github
|
||||||
from flask_dance.contrib.google import make_google_blueprint, google
|
from flask_dance.contrib.google import make_google_blueprint, google
|
||||||
|
from oauthlib.oauth2 import TokenExpiredError, InvalidGrantError
|
||||||
from flask_login import login_user, current_user, login_required
|
from flask_login import login_user, current_user, login_required
|
||||||
from sqlalchemy.orm.exc import NoResultFound
|
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 = {}
|
||||||
|
oauthblueprints = []
|
||||||
oauth = Blueprint('oauth', __name__)
|
oauth = Blueprint('oauth', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@ def register_user_with_oauth(user=None):
|
|||||||
except NoResultFound:
|
except NoResultFound:
|
||||||
# no found, return error
|
# no found, return error
|
||||||
return
|
return
|
||||||
ub.session_commit("User {} with OAuth for provider {} registered".format(user.nickname, oauth_key))
|
ub.session_commit("User {} with OAuth for provider {} registered".format(user.name, oauth_key))
|
||||||
|
|
||||||
|
|
||||||
def logout_oauth_user():
|
def logout_oauth_user():
|
||||||
@@ -93,8 +98,115 @@ def logout_oauth_user():
|
|||||||
session.pop(str(oauth_key) + '_oauth_user_id')
|
session.pop(str(oauth_key) + '_oauth_user_id')
|
||||||
|
|
||||||
|
|
||||||
if ub.oauth_support:
|
def oauth_update_token(provider_id, token, provider_user_id):
|
||||||
oauthblueprints = []
|
session[provider_id + "_oauth_user_id"] = provider_user_id
|
||||||
|
session[provider_id + "_oauth_token"] = token
|
||||||
|
|
||||||
|
# Find this OAuth token in the database, or create it
|
||||||
|
query = ub.session.query(ub.OAuth).filter_by(
|
||||||
|
provider=provider_id,
|
||||||
|
provider_user_id=provider_user_id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
oauth_entry = query.one()
|
||||||
|
# update token
|
||||||
|
oauth_entry.token = token
|
||||||
|
except NoResultFound:
|
||||||
|
oauth_entry = ub.OAuth(
|
||||||
|
provider=provider_id,
|
||||||
|
provider_user_id=provider_user_id,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
ub.session.add(oauth_entry)
|
||||||
|
ub.session_commit()
|
||||||
|
|
||||||
|
# Disable Flask-Dance's default behavior for saving the OAuth token
|
||||||
|
# Value differrs depending on flask-dance version
|
||||||
|
return backend_resultcode
|
||||||
|
|
||||||
|
|
||||||
|
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
|
||||||
|
query = ub.session.query(ub.OAuth).filter_by(
|
||||||
|
provider=provider_id,
|
||||||
|
provider_user_id=provider_user_id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
oauth_entry = query.first()
|
||||||
|
# already bind with user, just login
|
||||||
|
if oauth_entry.user:
|
||||||
|
login_user(oauth_entry.user)
|
||||||
|
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name)
|
||||||
|
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name),
|
||||||
|
category="success")
|
||||||
|
return redirect(url_for('web.index'))
|
||||||
|
else:
|
||||||
|
# bind to current user
|
||||||
|
if current_user and current_user.is_authenticated:
|
||||||
|
oauth_entry.user = current_user
|
||||||
|
try:
|
||||||
|
ub.session.add(oauth_entry)
|
||||||
|
ub.session.commit()
|
||||||
|
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
||||||
|
log.info("Link to {} Succeeded".format(provider_name))
|
||||||
|
return redirect(url_for('web.profile'))
|
||||||
|
except Exception as ex:
|
||||||
|
log.debug_or_exception(ex)
|
||||||
|
ub.session.rollback()
|
||||||
|
else:
|
||||||
|
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
||||||
|
log.info('Login failed, No User Linked With OAuth Account')
|
||||||
|
return redirect(url_for('web.login'))
|
||||||
|
# return redirect(url_for('web.login'))
|
||||||
|
# if config.config_public_reg:
|
||||||
|
# return redirect(url_for('web.register'))
|
||||||
|
# else:
|
||||||
|
# flash(_(u"Public registration is not enabled"), category="error")
|
||||||
|
# return redirect(url_for(redirect_url))
|
||||||
|
except (NoResultFound, AttributeError):
|
||||||
|
return redirect(url_for(redirect_url))
|
||||||
|
|
||||||
|
|
||||||
|
def get_oauth_status():
|
||||||
|
status = []
|
||||||
|
query = ub.session.query(ub.OAuth).filter_by(
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
oauths = query.all()
|
||||||
|
for oauth_entry in oauths:
|
||||||
|
status.append(int(oauth_entry.provider))
|
||||||
|
return status
|
||||||
|
except NoResultFound:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def unlink_oauth(provider):
|
||||||
|
if request.host_url + 'me' != request.referrer:
|
||||||
|
pass
|
||||||
|
query = ub.session.query(ub.OAuth).filter_by(
|
||||||
|
provider=provider,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
oauth_entry = query.one()
|
||||||
|
if current_user and current_user.is_authenticated:
|
||||||
|
oauth_entry.user = current_user
|
||||||
|
try:
|
||||||
|
ub.session.delete(oauth_entry)
|
||||||
|
ub.session.commit()
|
||||||
|
logout_oauth_user()
|
||||||
|
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
||||||
|
log.info("Unlink to {} Succeeded".format(oauth_check[provider]))
|
||||||
|
except Exception as ex:
|
||||||
|
log.debug_or_exception(ex)
|
||||||
|
ub.session.rollback()
|
||||||
|
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
||||||
|
except NoResultFound:
|
||||||
|
log.warning("oauth %s for user %d not found", provider, current_user.id)
|
||||||
|
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
|
||||||
|
return redirect(url_for('web.profile'))
|
||||||
|
|
||||||
|
def generate_oauth_blueprints():
|
||||||
if not ub.session.query(ub.OAuthProvider).count():
|
if not ub.session.query(ub.OAuthProvider).count():
|
||||||
for provider in ("github", "google"):
|
for provider in ("github", "google"):
|
||||||
oauthProvider = ub.OAuthProvider()
|
oauthProvider = ub.OAuthProvider()
|
||||||
@@ -138,17 +250,23 @@ if ub.oauth_support:
|
|||||||
app.register_blueprint(blueprint, url_prefix="/login")
|
app.register_blueprint(blueprint, url_prefix="/login")
|
||||||
if element['active']:
|
if element['active']:
|
||||||
register_oauth_blueprint(element['id'], element['provider_name'])
|
register_oauth_blueprint(element['id'], element['provider_name'])
|
||||||
|
return oauthblueprints
|
||||||
|
|
||||||
|
|
||||||
|
if ub.oauth_support:
|
||||||
|
oauthblueprints = generate_oauth_blueprints()
|
||||||
|
|
||||||
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
|
@oauth_authorized.connect_via(oauthblueprints[0]['blueprint'])
|
||||||
def github_logged_in(blueprint, token):
|
def github_logged_in(blueprint, token):
|
||||||
if not token:
|
if not token:
|
||||||
flash(_(u"Failed to log in with GitHub."), category="error")
|
flash(_(u"Failed to log in with GitHub."), category="error")
|
||||||
|
log.error("Failed to log in with GitHub")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
resp = blueprint.session.get("/user")
|
resp = blueprint.session.get("/user")
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
flash(_(u"Failed to fetch user info from GitHub."), category="error")
|
flash(_(u"Failed to fetch user info from GitHub."), category="error")
|
||||||
|
log.error("Failed to fetch user info from GitHub")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
github_info = resp.json()
|
github_info = resp.json()
|
||||||
@@ -160,11 +278,13 @@ if ub.oauth_support:
|
|||||||
def google_logged_in(blueprint, token):
|
def google_logged_in(blueprint, token):
|
||||||
if not token:
|
if not token:
|
||||||
flash(_(u"Failed to log in with Google."), category="error")
|
flash(_(u"Failed to log in with Google."), category="error")
|
||||||
|
log.error("Failed to log in with Google")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
resp = blueprint.session.get("/oauth2/v2/userinfo")
|
resp = blueprint.session.get("/oauth2/v2/userinfo")
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
flash(_(u"Failed to fetch user info from Google."), category="error")
|
flash(_(u"Failed to fetch user info from Google."), category="error")
|
||||||
|
log.error("Failed to fetch user info from Google")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
google_info = resp.json()
|
google_info = resp.json()
|
||||||
@@ -172,113 +292,6 @@ if ub.oauth_support:
|
|||||||
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
|
return oauth_update_token(str(oauthblueprints[1]['id']), token, google_user_id)
|
||||||
|
|
||||||
|
|
||||||
def oauth_update_token(provider_id, token, provider_user_id):
|
|
||||||
session[provider_id + "_oauth_user_id"] = provider_user_id
|
|
||||||
session[provider_id + "_oauth_token"] = token
|
|
||||||
|
|
||||||
# Find this OAuth token in the database, or create it
|
|
||||||
query = ub.session.query(ub.OAuth).filter_by(
|
|
||||||
provider=provider_id,
|
|
||||||
provider_user_id=provider_user_id,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
oauth_entry = query.one()
|
|
||||||
# update token
|
|
||||||
oauth_entry.token = token
|
|
||||||
except NoResultFound:
|
|
||||||
oauth_entry = ub.OAuth(
|
|
||||||
provider=provider_id,
|
|
||||||
provider_user_id=provider_user_id,
|
|
||||||
token=token,
|
|
||||||
)
|
|
||||||
ub.session.add(oauth_entry)
|
|
||||||
ub.session_commit()
|
|
||||||
|
|
||||||
# Disable Flask-Dance's default behavior for saving the OAuth token
|
|
||||||
# Value differrs depending on flask-dance version
|
|
||||||
return backend_resultcode
|
|
||||||
|
|
||||||
|
|
||||||
def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider_name):
|
|
||||||
query = ub.session.query(ub.OAuth).filter_by(
|
|
||||||
provider=provider_id,
|
|
||||||
provider_user_id=provider_user_id,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
oauth_entry = query.first()
|
|
||||||
# already bind with user, just login
|
|
||||||
if oauth_entry.user:
|
|
||||||
login_user(oauth_entry.user)
|
|
||||||
log.debug(u"You are now logged in as: '%s'", oauth_entry.user.nickname)
|
|
||||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.nickname),
|
|
||||||
category="success")
|
|
||||||
return redirect(url_for('web.index'))
|
|
||||||
else:
|
|
||||||
# bind to current user
|
|
||||||
if current_user and current_user.is_authenticated:
|
|
||||||
oauth_entry.user = current_user
|
|
||||||
try:
|
|
||||||
ub.session.add(oauth_entry)
|
|
||||||
ub.session.commit()
|
|
||||||
flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success")
|
|
||||||
return redirect(url_for('web.profile'))
|
|
||||||
except Exception as e:
|
|
||||||
log.debug_or_exception(e)
|
|
||||||
ub.session.rollback()
|
|
||||||
else:
|
|
||||||
flash(_(u"Login failed, No User Linked With OAuth Account"), category="error")
|
|
||||||
log.info('Login failed, No User Linked With OAuth Account')
|
|
||||||
return redirect(url_for('web.login'))
|
|
||||||
# return redirect(url_for('web.login'))
|
|
||||||
# if config.config_public_reg:
|
|
||||||
# return redirect(url_for('web.register'))
|
|
||||||
# else:
|
|
||||||
# flash(_(u"Public registration is not enabled"), category="error")
|
|
||||||
# return redirect(url_for(redirect_url))
|
|
||||||
except (NoResultFound, AttributeError):
|
|
||||||
return redirect(url_for(redirect_url))
|
|
||||||
|
|
||||||
|
|
||||||
def get_oauth_status():
|
|
||||||
status = []
|
|
||||||
query = ub.session.query(ub.OAuth).filter_by(
|
|
||||||
user_id=current_user.id,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
oauths = query.all()
|
|
||||||
for oauth_entry in oauths:
|
|
||||||
status.append(int(oauth_entry.provider))
|
|
||||||
return status
|
|
||||||
except NoResultFound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def unlink_oauth(provider):
|
|
||||||
if request.host_url + 'me' != request.referrer:
|
|
||||||
pass
|
|
||||||
query = ub.session.query(ub.OAuth).filter_by(
|
|
||||||
provider=provider,
|
|
||||||
user_id=current_user.id,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
oauth_entry = query.one()
|
|
||||||
if current_user and current_user.is_authenticated:
|
|
||||||
oauth_entry.user = current_user
|
|
||||||
try:
|
|
||||||
ub.session.delete(oauth_entry)
|
|
||||||
ub.session.commit()
|
|
||||||
logout_oauth_user()
|
|
||||||
flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success")
|
|
||||||
except Exception as e:
|
|
||||||
log.debug_or_exception(e)
|
|
||||||
ub.session.rollback()
|
|
||||||
flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error")
|
|
||||||
except NoResultFound:
|
|
||||||
log.warning("oauth %s for user %d not found", provider, current_user.id)
|
|
||||||
flash(_(u"Not Linked to %(oauth)s", oauth=provider), category="error")
|
|
||||||
return redirect(url_for('web.profile'))
|
|
||||||
|
|
||||||
|
|
||||||
# notify on OAuth provider error
|
# notify on OAuth provider error
|
||||||
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
|
@oauth_error.connect_via(oauthblueprints[0]['blueprint'])
|
||||||
def github_error(blueprint, error, error_description=None, error_uri=None):
|
def github_error(blueprint, error, error_description=None, error_uri=None):
|
||||||
@@ -293,39 +306,6 @@ if ub.oauth_support:
|
|||||||
) # ToDo: Translate
|
) # ToDo: Translate
|
||||||
flash(msg, category="error")
|
flash(msg, category="error")
|
||||||
|
|
||||||
|
|
||||||
@oauth.route('/link/github')
|
|
||||||
@oauth_required
|
|
||||||
def github_login():
|
|
||||||
if not github.authorized:
|
|
||||||
return redirect(url_for('github.login'))
|
|
||||||
account_info = github.get('/user')
|
|
||||||
if account_info.ok:
|
|
||||||
account_info_json = account_info.json()
|
|
||||||
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
|
|
||||||
flash(_(u"GitHub Oauth error, please retry later."), category="error")
|
|
||||||
return redirect(url_for('web.login'))
|
|
||||||
|
|
||||||
|
|
||||||
@oauth.route('/unlink/github', methods=["GET"])
|
|
||||||
@login_required
|
|
||||||
def github_login_unlink():
|
|
||||||
return unlink_oauth(oauthblueprints[0]['id'])
|
|
||||||
|
|
||||||
|
|
||||||
@oauth.route('/link/google')
|
|
||||||
@oauth_required
|
|
||||||
def google_login():
|
|
||||||
if not google.authorized:
|
|
||||||
return redirect(url_for("google.login"))
|
|
||||||
resp = google.get("/oauth2/v2/userinfo")
|
|
||||||
if resp.ok:
|
|
||||||
account_info_json = resp.json()
|
|
||||||
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
|
|
||||||
flash(_(u"Google Oauth error, please retry later."), category="error")
|
|
||||||
return redirect(url_for('web.login'))
|
|
||||||
|
|
||||||
|
|
||||||
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
|
@oauth_error.connect_via(oauthblueprints[1]['blueprint'])
|
||||||
def google_error(blueprint, error, error_description=None, error_uri=None):
|
def google_error(blueprint, error, error_description=None, error_uri=None):
|
||||||
msg = (
|
msg = (
|
||||||
@@ -340,7 +320,49 @@ if ub.oauth_support:
|
|||||||
flash(msg, category="error")
|
flash(msg, category="error")
|
||||||
|
|
||||||
|
|
||||||
@oauth.route('/unlink/google', methods=["GET"])
|
@oauth.route('/link/github')
|
||||||
@login_required
|
@oauth_required
|
||||||
def google_login_unlink():
|
def github_login():
|
||||||
|
if not github.authorized:
|
||||||
|
return redirect(url_for('github.login'))
|
||||||
|
try:
|
||||||
|
account_info = github.get('/user')
|
||||||
|
if account_info.ok:
|
||||||
|
account_info_json = account_info.json()
|
||||||
|
return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github')
|
||||||
|
flash(_(u"GitHub Oauth error, please retry later."), category="error")
|
||||||
|
log.error("GitHub Oauth error, please retry later")
|
||||||
|
except (InvalidGrantError, TokenExpiredError) as e:
|
||||||
|
flash(_(u"GitHub Oauth error: {}").format(e), category="error")
|
||||||
|
log.error(e)
|
||||||
|
return redirect(url_for('web.login'))
|
||||||
|
|
||||||
|
|
||||||
|
@oauth.route('/unlink/github', methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def github_login_unlink():
|
||||||
|
return unlink_oauth(oauthblueprints[0]['id'])
|
||||||
|
|
||||||
|
|
||||||
|
@oauth.route('/link/google')
|
||||||
|
@oauth_required
|
||||||
|
def google_login():
|
||||||
|
if not google.authorized:
|
||||||
|
return redirect(url_for("google.login"))
|
||||||
|
try:
|
||||||
|
resp = google.get("/oauth2/v2/userinfo")
|
||||||
|
if resp.ok:
|
||||||
|
account_info_json = resp.json()
|
||||||
|
return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google')
|
||||||
|
flash(_(u"Google Oauth error, please retry later."), category="error")
|
||||||
|
log.error("Google Oauth error, please retry later")
|
||||||
|
except (InvalidGrantError, TokenExpiredError) as e:
|
||||||
|
flash(_(u"Google Oauth error: {}").format(e), category="error")
|
||||||
|
log.error(e)
|
||||||
|
return redirect(url_for('web.login'))
|
||||||
|
|
||||||
|
|
||||||
|
@oauth.route('/unlink/google', methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def google_login_unlink():
|
||||||
return unlink_oauth(oauthblueprints[1]['id'])
|
return unlink_oauth(oauthblueprints[1]['id'])
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from functools import wraps
|
|||||||
|
|
||||||
from flask import Blueprint, request, render_template, Response, g, make_response, abort
|
from flask import Blueprint, request, render_template, Response, g, make_response, abort
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
from sqlalchemy.sql.expression import func, text, or_, and_
|
from sqlalchemy.sql.expression import func, text, or_, and_, true
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
|
from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages
|
||||||
@@ -94,7 +94,45 @@ def feed_cc_search(query):
|
|||||||
@opds.route("/opds/search", methods=["GET"])
|
@opds.route("/opds/search", methods=["GET"])
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_normal_search():
|
def feed_normal_search():
|
||||||
return feed_search(request.args.get("query").strip())
|
return feed_search(request.args.get("query", "").strip())
|
||||||
|
|
||||||
|
|
||||||
|
@opds.route("/opds/books")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_booksindex():
|
||||||
|
shift = 0
|
||||||
|
off = int(request.args.get("offset") or 0)
|
||||||
|
entries = calibre_db.session.query(func.upper(func.substr(db.Books.sort, 1, 1)).label('id'))\
|
||||||
|
.filter(calibre_db.common_filters()).group_by(func.upper(func.substr(db.Books.sort, 1, 1))).all()
|
||||||
|
|
||||||
|
elements = []
|
||||||
|
if off == 0:
|
||||||
|
elements.append({'id': "00", 'name':_("All")})
|
||||||
|
shift = 1
|
||||||
|
for entry in entries[
|
||||||
|
off + shift - 1:
|
||||||
|
int(off + int(config.config_books_per_page) - shift)]:
|
||||||
|
elements.append({'id': entry.id, 'name': entry.id})
|
||||||
|
|
||||||
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
|
len(entries) + 1)
|
||||||
|
return render_xml_template('feed.xml',
|
||||||
|
letterelements=elements,
|
||||||
|
folder='opds.feed_letter_books',
|
||||||
|
pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
|
@opds.route("/opds/books/letter/<book_id>")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_letter_books(book_id):
|
||||||
|
off = request.args.get("offset") or 0
|
||||||
|
letter = true() if book_id == "00" else func.upper(db.Books.sort).startswith(book_id)
|
||||||
|
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
|
||||||
|
db.Books,
|
||||||
|
letter,
|
||||||
|
[db.Books.sort])
|
||||||
|
|
||||||
|
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@opds.route("/opds/new")
|
@opds.route("/opds/new")
|
||||||
@@ -150,14 +188,41 @@ def feed_hot():
|
|||||||
@opds.route("/opds/author")
|
@opds.route("/opds/author")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_authorindex():
|
def feed_authorindex():
|
||||||
off = request.args.get("offset") or 0
|
shift = 0
|
||||||
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
|
off = int(request.args.get("offset") or 0)
|
||||||
.filter(calibre_db.common_filters())\
|
entries = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('id'))\
|
||||||
.group_by(text('books_authors_link.author'))\
|
.join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters())\
|
||||||
.order_by(db.Authors.sort).limit(config.config_books_per_page)\
|
.group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all()
|
||||||
.offset(off)
|
|
||||||
|
elements = []
|
||||||
|
if off == 0:
|
||||||
|
elements.append({'id': "00", 'name':_("All")})
|
||||||
|
shift = 1
|
||||||
|
for entry in entries[
|
||||||
|
off + shift - 1:
|
||||||
|
int(off + int(config.config_books_per_page) - shift)]:
|
||||||
|
elements.append({'id': entry.id, 'name': entry.id})
|
||||||
|
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(calibre_db.session.query(db.Authors).all()))
|
len(entries) + 1)
|
||||||
|
return render_xml_template('feed.xml',
|
||||||
|
letterelements=elements,
|
||||||
|
folder='opds.feed_letter_author',
|
||||||
|
pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
|
@opds.route("/opds/author/letter/<book_id>")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_letter_author(book_id):
|
||||||
|
off = request.args.get("offset") or 0
|
||||||
|
letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id)
|
||||||
|
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
|
||||||
|
.filter(calibre_db.common_filters()).filter(letter)\
|
||||||
|
.group_by(text('books_authors_link.author'))\
|
||||||
|
.order_by(db.Authors.sort)
|
||||||
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
|
entries.count())
|
||||||
|
entries = entries.limit(config.config_books_per_page).offset(off).all()
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -201,17 +266,41 @@ def feed_publisher(book_id):
|
|||||||
@opds.route("/opds/category")
|
@opds.route("/opds/category")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_categoryindex():
|
def feed_categoryindex():
|
||||||
|
shift = 0
|
||||||
|
off = int(request.args.get("offset") or 0)
|
||||||
|
entries = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('id'))\
|
||||||
|
.join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters())\
|
||||||
|
.group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all()
|
||||||
|
elements = []
|
||||||
|
if off == 0:
|
||||||
|
elements.append({'id': "00", 'name':_("All")})
|
||||||
|
shift = 1
|
||||||
|
for entry in entries[
|
||||||
|
off + shift - 1:
|
||||||
|
int(off + int(config.config_books_per_page) - shift)]:
|
||||||
|
elements.append({'id': entry.id, 'name': entry.id})
|
||||||
|
|
||||||
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
|
len(entries) + 1)
|
||||||
|
return render_xml_template('feed.xml',
|
||||||
|
letterelements=elements,
|
||||||
|
folder='opds.feed_letter_category',
|
||||||
|
pagination=pagination)
|
||||||
|
|
||||||
|
@opds.route("/opds/category/letter/<book_id>")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_letter_category(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
|
letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id)
|
||||||
entries = calibre_db.session.query(db.Tags)\
|
entries = calibre_db.session.query(db.Tags)\
|
||||||
.join(db.books_tags_link)\
|
.join(db.books_tags_link)\
|
||||||
.join(db.Books)\
|
.join(db.Books)\
|
||||||
.filter(calibre_db.common_filters())\
|
.filter(calibre_db.common_filters()).filter(letter)\
|
||||||
.group_by(text('books_tags_link.tag'))\
|
.group_by(text('books_tags_link.tag'))\
|
||||||
.order_by(db.Tags.name)\
|
.order_by(db.Tags.name)
|
||||||
.offset(off)\
|
|
||||||
.limit(config.config_books_per_page)
|
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(calibre_db.session.query(db.Tags).all()))
|
entries.count())
|
||||||
|
entries = entries.offset(off).limit(config.config_books_per_page).all()
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -229,16 +318,40 @@ def feed_category(book_id):
|
|||||||
@opds.route("/opds/series")
|
@opds.route("/opds/series")
|
||||||
@requires_basic_auth_if_no_ano
|
@requires_basic_auth_if_no_ano
|
||||||
def feed_seriesindex():
|
def feed_seriesindex():
|
||||||
|
shift = 0
|
||||||
|
off = int(request.args.get("offset") or 0)
|
||||||
|
entries = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('id'))\
|
||||||
|
.join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters())\
|
||||||
|
.group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all()
|
||||||
|
elements = []
|
||||||
|
if off == 0:
|
||||||
|
elements.append({'id': "00", 'name':_("All")})
|
||||||
|
shift = 1
|
||||||
|
for entry in entries[
|
||||||
|
off + shift - 1:
|
||||||
|
int(off + int(config.config_books_per_page) - shift)]:
|
||||||
|
elements.append({'id': entry.id, 'name': entry.id})
|
||||||
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
|
len(entries) + 1)
|
||||||
|
return render_xml_template('feed.xml',
|
||||||
|
letterelements=elements,
|
||||||
|
folder='opds.feed_letter_series',
|
||||||
|
pagination=pagination)
|
||||||
|
|
||||||
|
@opds.route("/opds/series/letter/<book_id>")
|
||||||
|
@requires_basic_auth_if_no_ano
|
||||||
|
def feed_letter_series(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
|
letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id)
|
||||||
entries = calibre_db.session.query(db.Series)\
|
entries = calibre_db.session.query(db.Series)\
|
||||||
.join(db.books_series_link)\
|
.join(db.books_series_link)\
|
||||||
.join(db.Books)\
|
.join(db.Books)\
|
||||||
.filter(calibre_db.common_filters())\
|
.filter(calibre_db.common_filters()).filter(letter)\
|
||||||
.group_by(text('books_series_link.series'))\
|
.group_by(text('books_series_link.series'))\
|
||||||
.order_by(db.Series.sort)\
|
.order_by(db.Series.sort)
|
||||||
.offset(off).all()
|
|
||||||
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
|
||||||
len(calibre_db.session.query(db.Series).all()))
|
entries.count())
|
||||||
|
entries = entries.offset(off).limit(config.config_books_per_page).all()
|
||||||
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -269,7 +382,7 @@ def feed_ratingindex():
|
|||||||
len(entries))
|
len(entries))
|
||||||
element = list()
|
element = list()
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
element.append(FeedObject(entry[0].id, "{} Stars".format(entry.name)))
|
element.append(FeedObject(entry[0].id, _("{} Stars").format(entry.name)))
|
||||||
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination)
|
return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
@@ -428,9 +541,14 @@ def check_auth(username, password):
|
|||||||
username = username.encode('windows-1252')
|
username = username.encode('windows-1252')
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
username = username.encode('utf-8')
|
username = username.encode('utf-8')
|
||||||
user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) ==
|
user = ub.session.query(ub.User).filter(func.lower(ub.User.name) ==
|
||||||
username.decode('utf-8').lower()).first()
|
username.decode('utf-8').lower()).first()
|
||||||
return bool(user and check_password_hash(str(user.password), password))
|
if bool(user and check_password_hash(str(user.password), password)):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
ip_Address = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||||
|
log.warning('OPDS Login failed for user "%s" IP-address: %s', username.decode('utf-8'), ip_Address)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def authenticate():
|
def authenticate():
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def remote_login():
|
|||||||
ub.session_commit()
|
ub.session_commit()
|
||||||
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
|
verify_url = url_for('remotelogin.verify_token', token=auth_token.auth_token, _external=true)
|
||||||
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
|
log.debug(u"Remot Login request with token: %s", auth_token.auth_token)
|
||||||
return render_title_template('remote_login.html', title=_(u"login"), token=auth_token.auth_token,
|
return render_title_template('remote_login.html', title=_(u"Login"), token=auth_token.auth_token,
|
||||||
verify_url=verify_url, page="remotelogin")
|
verify_url=verify_url, page="remotelogin")
|
||||||
|
|
||||||
|
|
||||||
@@ -126,11 +126,11 @@ def token_verified():
|
|||||||
login_user(user)
|
login_user(user)
|
||||||
|
|
||||||
ub.session.delete(auth_token)
|
ub.session.delete(auth_token)
|
||||||
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.nickname))
|
ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name))
|
||||||
|
|
||||||
data['status'] = 'success'
|
data['status'] = 'success'
|
||||||
log.debug(u"Remote Login for userid %s succeded", user.id)
|
log.debug(u"Remote Login for userid %s succeded", user.id)
|
||||||
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success")
|
flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success")
|
||||||
|
|
||||||
response = make_response(json.dumps(data, ensure_ascii=False))
|
response = make_response(json.dumps(data, ensure_ascii=False))
|
||||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||||
|
|||||||
@@ -36,12 +36,18 @@ def get_sidebar_config(kwargs=None):
|
|||||||
else:
|
else:
|
||||||
content = 'conf' in kwargs
|
content = 'conf' in kwargs
|
||||||
sidebar = list()
|
sidebar = list()
|
||||||
sidebar.append({"glyph": "glyphicon-book", "text": _('Recently Added'), "link": 'web.index', "id": "new",
|
sidebar.append({"glyph": "glyphicon-book", "text": _('Books'), "link": 'web.index', "id": "new",
|
||||||
"visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root",
|
"visibility": constants.SIDEBAR_RECENT, 'public': True, "page": "root",
|
||||||
"show_text": _('Show recent books'), "config_show":False})
|
"show_text": _('Show recent books'), "config_show":False})
|
||||||
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
|
sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot",
|
||||||
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
|
"visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot",
|
||||||
"show_text": _('Show Hot Books'), "config_show": True})
|
"show_text": _('Show Hot Books'), "config_show": True})
|
||||||
|
if current_user.role_admin():
|
||||||
|
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list',
|
||||||
|
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
|
||||||
|
"page": "download", "show_text": _('Show Downloaded Books'),
|
||||||
|
"config_show": content})
|
||||||
|
else:
|
||||||
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
|
sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list',
|
||||||
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
|
"id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous),
|
||||||
"page": "download", "show_text": _('Show Downloaded Books'),
|
"page": "download", "show_text": _('Show Downloaded Books'),
|
||||||
@@ -59,7 +65,7 @@ def get_sidebar_config(kwargs=None):
|
|||||||
"show_text": _('Show unread'), "config_show": False})
|
"show_text": _('Show unread'), "config_show": False})
|
||||||
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
|
sidebar.append({"glyph": "glyphicon-random", "text": _('Discover'), "link": 'web.books_list', "id": "rand",
|
||||||
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
|
"visibility": constants.SIDEBAR_RANDOM, 'public': True, "page": "discover",
|
||||||
"show_text": _('Show random books'), "config_show": True})
|
"show_text": _('Show Random Books'), "config_show": True})
|
||||||
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
sidebar.append({"glyph": "glyphicon-inbox", "text": _('Categories'), "link": 'web.category_list', "id": "cat",
|
||||||
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
|
"visibility": constants.SIDEBAR_CATEGORY, 'public': True, "page": "category",
|
||||||
"show_text": _('Show category selection'), "config_show": True})
|
"show_text": _('Show category selection'), "config_show": True})
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import os
|
|||||||
import errno
|
import errno
|
||||||
import signal
|
import signal
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess # nosec
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from gevent.pywsgi import WSGIServer
|
from gevent.pywsgi import WSGIServer
|
||||||
@@ -136,6 +137,64 @@ class WebServer(object):
|
|||||||
|
|
||||||
return sock, _readable_listen_address(*address)
|
return sock, _readable_listen_address(*address)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_args_for_reloading():
|
||||||
|
"""Determine how the script was executed, and return the args needed
|
||||||
|
to execute it again in a new process.
|
||||||
|
Code from https://github.com/pyload/pyload. Author GammaC0de, voulter
|
||||||
|
"""
|
||||||
|
rv = [sys.executable]
|
||||||
|
py_script = sys.argv[0]
|
||||||
|
args = sys.argv[1:]
|
||||||
|
# Need to look at main module to determine how it was executed.
|
||||||
|
__main__ = sys.modules["__main__"]
|
||||||
|
|
||||||
|
# The value of __package__ indicates how Python was called. It may
|
||||||
|
# not exist if a setuptools script is installed as an egg. It may be
|
||||||
|
# set incorrectly for entry points created with pip on Windows.
|
||||||
|
if getattr(__main__, "__package__", None) is None or (
|
||||||
|
os.name == "nt"
|
||||||
|
and __main__.__package__ == ""
|
||||||
|
and not os.path.exists(py_script)
|
||||||
|
and os.path.exists("{}.exe".format(py_script))
|
||||||
|
):
|
||||||
|
# Executed a file, like "python app.py".
|
||||||
|
py_script = os.path.abspath(py_script)
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
# Windows entry points have ".exe" extension and should be
|
||||||
|
# called directly.
|
||||||
|
if not os.path.exists(py_script) and os.path.exists("{}.exe".format(py_script)):
|
||||||
|
py_script += ".exe"
|
||||||
|
|
||||||
|
if (
|
||||||
|
os.path.splitext(sys.executable)[1] == ".exe"
|
||||||
|
and os.path.splitext(py_script)[1] == ".exe"
|
||||||
|
):
|
||||||
|
rv.pop(0)
|
||||||
|
|
||||||
|
rv.append(py_script)
|
||||||
|
else:
|
||||||
|
# Executed a module, like "python -m module".
|
||||||
|
if sys.argv[0] == "-m":
|
||||||
|
args = sys.argv
|
||||||
|
else:
|
||||||
|
if os.path.isfile(py_script):
|
||||||
|
# Rewritten by Python from "-m script" to "/path/to/script.py".
|
||||||
|
py_module = __main__.__package__
|
||||||
|
name = os.path.splitext(os.path.basename(py_script))[0]
|
||||||
|
|
||||||
|
if name != "__main__":
|
||||||
|
py_module += ".{}".format(name)
|
||||||
|
else:
|
||||||
|
# Incorrectly rewritten by pydevd debugger from "-m script" to "script".
|
||||||
|
py_module = py_script
|
||||||
|
|
||||||
|
rv.extend(("-m", py_module.lstrip(".")))
|
||||||
|
|
||||||
|
rv.extend(args)
|
||||||
|
return rv
|
||||||
|
|
||||||
def _start_gevent(self):
|
def _start_gevent(self):
|
||||||
ssl_args = self.ssl_args or {}
|
ssl_args = self.ssl_args or {}
|
||||||
|
|
||||||
@@ -192,18 +251,16 @@ class WebServer(object):
|
|||||||
finally:
|
finally:
|
||||||
self.wsgiserver = None
|
self.wsgiserver = None
|
||||||
|
|
||||||
|
# prevent irritating log of pending tasks message from asyncio
|
||||||
|
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
|
||||||
|
|
||||||
if not self.restart:
|
if not self.restart:
|
||||||
log.info("Performing shutdown of Calibre-Web")
|
log.info("Performing shutdown of Calibre-Web")
|
||||||
# prevent irritiating log of pending tasks message from asyncio
|
|
||||||
logger.get('asyncio').setLevel(logger.logging.CRITICAL)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
log.info("Performing restart of Calibre-Web")
|
log.info("Performing restart of Calibre-Web")
|
||||||
arguments = list(sys.argv)
|
args = self._get_args_for_reloading()
|
||||||
arguments.insert(0, sys.executable)
|
subprocess.call(args, close_fds=True) # nosec
|
||||||
if os.name == 'nt':
|
|
||||||
arguments = ["\"%s\"" % a for a in arguments]
|
|
||||||
os.execv(sys.executable, arguments)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _killServer(self, __, ___):
|
def _killServer(self, __, ___):
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from base64 import b64decode, b64encode
|
|||||||
from jsonschema import validate, exceptions, __version__
|
from jsonschema import validate, exceptions, __version__
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
try:
|
try:
|
||||||
|
# pylint: disable=unused-import
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
@@ -64,7 +65,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"
|
||||||
@@ -98,7 +99,7 @@ class SyncToken:
|
|||||||
reading_state_last_modified=datetime.min,
|
reading_state_last_modified=datetime.min,
|
||||||
tags_last_modified=datetime.min,
|
tags_last_modified=datetime.min,
|
||||||
books_last_id=-1
|
books_last_id=-1
|
||||||
):
|
): # nosec
|
||||||
self.raw_kobo_store_token = raw_kobo_store_token
|
self.raw_kobo_store_token = raw_kobo_store_token
|
||||||
self.books_last_created = books_last_created
|
self.books_last_created = books_last_created
|
||||||
self.books_last_modified = books_last_modified
|
self.books_last_modified = books_last_modified
|
||||||
@@ -110,7 +111,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
|
||||||
|
|||||||
@@ -45,3 +45,9 @@ except ImportError as err:
|
|||||||
log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
|
log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err)
|
||||||
kobo = None
|
kobo = None
|
||||||
SyncToken = None
|
SyncToken = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import gmail
|
||||||
|
except ImportError as err:
|
||||||
|
log.debug("Cannot import gmail, sending books via Gmail Oauth2 Verification will not work: %s", err)
|
||||||
|
gmail = None
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
import os.path
|
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
|
from google.auth.transport.requests import Request
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from google.oauth2.credentials import Credentials
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import base64
|
||||||
|
from flask_babel import gettext as _
|
||||||
|
from ..constants import BASE_DIR
|
||||||
|
from .. import logger
|
||||||
|
|
||||||
|
|
||||||
|
log = logger.create()
|
||||||
|
|
||||||
|
SCOPES = ['openid', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/userinfo.email']
|
||||||
|
|
||||||
|
def setup_gmail(token):
|
||||||
|
# If there are no (valid) credentials available, let the user log in.
|
||||||
|
creds = None
|
||||||
|
if "token" in token:
|
||||||
|
creds = Credentials(
|
||||||
|
token=token['token'],
|
||||||
|
refresh_token=token['refresh_token'],
|
||||||
|
token_uri=token['token_uri'],
|
||||||
|
client_id=token['client_id'],
|
||||||
|
client_secret=token['client_secret'],
|
||||||
|
scopes=token['scopes'],
|
||||||
|
)
|
||||||
|
creds.expiry = datetime.fromisoformat(token['expiry'])
|
||||||
|
|
||||||
|
if not creds or not creds.valid:
|
||||||
|
# don't forget to dump one more time after the refresh
|
||||||
|
# also, some file-locking routines wouldn't be needless
|
||||||
|
if creds and creds.expired and creds.refresh_token:
|
||||||
|
creds.refresh(Request())
|
||||||
|
else:
|
||||||
|
cred_file = os.path.join(BASE_DIR, 'gmail.json')
|
||||||
|
if not os.path.exists(cred_file):
|
||||||
|
raise Exception(_("Found no valid gmail.json file with OAuth information"))
|
||||||
|
flow = InstalledAppFlow.from_client_secrets_file(
|
||||||
|
os.path.join(BASE_DIR, 'gmail.json'), SCOPES)
|
||||||
|
creds = flow.run_local_server(port=0)
|
||||||
|
user_info = get_user_info(creds)
|
||||||
|
return {
|
||||||
|
'token': creds.token,
|
||||||
|
'refresh_token': creds.refresh_token,
|
||||||
|
'token_uri': creds.token_uri,
|
||||||
|
'client_id': creds.client_id,
|
||||||
|
'client_secret': creds.client_secret,
|
||||||
|
'scopes': creds.scopes,
|
||||||
|
'expiry': creds.expiry.isoformat(),
|
||||||
|
'email': user_info
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_user_info(credentials):
|
||||||
|
user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials)
|
||||||
|
user_info = user_info_service.userinfo().get().execute()
|
||||||
|
return user_info.get('email', "")
|
||||||
|
|
||||||
|
def send_messsage(token, msg):
|
||||||
|
log.debug("Start sending email via Gmail")
|
||||||
|
creds = Credentials(
|
||||||
|
token=token['token'],
|
||||||
|
refresh_token=token['refresh_token'],
|
||||||
|
token_uri=token['token_uri'],
|
||||||
|
client_id=token['client_id'],
|
||||||
|
client_secret=token['client_secret'],
|
||||||
|
scopes=token['scopes'],
|
||||||
|
)
|
||||||
|
creds.expiry = datetime.fromisoformat(token['expiry'])
|
||||||
|
if creds and creds.expired and creds.refresh_token:
|
||||||
|
creds.refresh(Request())
|
||||||
|
service = build('gmail', 'v1', credentials=creds)
|
||||||
|
message_as_bytes = msg.as_bytes() # the message should converted from string to bytes.
|
||||||
|
message_as_base64 = base64.urlsafe_b64encode(message_as_bytes) # encode in base64 (printable letters coding)
|
||||||
|
raw = message_as_base64.decode() # convert to something JSON serializable
|
||||||
|
body = {'raw': raw}
|
||||||
|
|
||||||
|
(service.users().messages().send(userId='me', body=body).execute())
|
||||||
|
log.debug("Email send successfully via Gmail")
|
||||||
@@ -45,7 +45,7 @@ class ImprovedQueue(queue.Queue):
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
return list(self.queue)
|
return list(self.queue)
|
||||||
|
|
||||||
#Class for all worker tasks in the background
|
# Class for all worker tasks in the background
|
||||||
class WorkerThread(threading.Thread):
|
class WorkerThread(threading.Thread):
|
||||||
_instance = None
|
_instance = None
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ class WorkerThread(threading.Thread):
|
|||||||
def add(cls, user, task):
|
def add(cls, user, task):
|
||||||
ins = cls.getInstance()
|
ins = cls.getInstance()
|
||||||
ins.num += 1
|
ins.num += 1
|
||||||
|
log.debug("Add Task for user: {}: {}".format(user, task))
|
||||||
ins.queue.put(QueuedTask(
|
ins.queue.put(QueuedTask(
|
||||||
num=ins.num,
|
num=ins.num,
|
||||||
user=user,
|
user=user,
|
||||||
@@ -159,9 +160,9 @@ class CalibreTask:
|
|||||||
# catch any unhandled exceptions in a task and automatically fail it
|
# catch any unhandled exceptions in a task and automatically fail it
|
||||||
try:
|
try:
|
||||||
self.run(*args)
|
self.run(*args)
|
||||||
except Exception as e:
|
except Exception as ex:
|
||||||
self._handleError(str(e))
|
self._handleError(str(ex))
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
|
|
||||||
self.end_time = datetime.now()
|
self.end_time = datetime.now()
|
||||||
|
|
||||||
|
|||||||
@@ -99,12 +99,14 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
|
log.error("Settings DB is not Writeable")
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
flash(_(u"Settings DB is not Writeable"), category="error")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
if not xhr:
|
if not xhr:
|
||||||
|
log.debug("Book has been added to shelf: {}".format(shelf.name))
|
||||||
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
@@ -123,6 +125,7 @@ def search_to_shelf(shelf_id):
|
|||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
if not check_shelf_edit_permissions(shelf):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
|
log.warning("You are not allowed to add a book to the the shelf: {}".format(shelf.name))
|
||||||
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
|
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
@@ -140,7 +143,7 @@ def search_to_shelf(shelf_id):
|
|||||||
books_for_shelf = ub.searched_ids[current_user.id]
|
books_for_shelf = ub.searched_ids[current_user.id]
|
||||||
|
|
||||||
if not books_for_shelf:
|
if not books_for_shelf:
|
||||||
log.error("Books are already part of %s", shelf.name)
|
log.error("Books are already part of {}".format(shelf.name))
|
||||||
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
flash(_(u"Books are already part of the shelf: %(name)s", name=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
@@ -156,8 +159,10 @@ def search_to_shelf(shelf_id):
|
|||||||
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
else:
|
else:
|
||||||
|
log.error("Could not add books to shelf: {}".format(shelf.name))
|
||||||
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
|
flash(_(u"Could not add books to shelf: %(sname)s", sname=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
@@ -168,7 +173,7 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
if shelf is None:
|
if shelf is None:
|
||||||
log.error("Invalid shelf specified: %s", shelf_id)
|
log.error("Invalid shelf specified: {}".format(shelf_id))
|
||||||
if not xhr:
|
if not xhr:
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Invalid shelf specified", 400
|
return "Invalid shelf specified", 400
|
||||||
@@ -197,7 +202,8 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
if "HTTP_REFERER" in request.environ:
|
if "HTTP_REFERER" in request.environ:
|
||||||
return redirect(request.environ["HTTP_REFERER"])
|
return redirect(request.environ["HTTP_REFERER"])
|
||||||
else:
|
else:
|
||||||
@@ -211,6 +217,7 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
return "", 204
|
return "", 204
|
||||||
else:
|
else:
|
||||||
if not xhr:
|
if not xhr:
|
||||||
|
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name))
|
||||||
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
|
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
|
||||||
category="error")
|
category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
@@ -255,13 +262,14 @@ def create_edit_shelf(shelf, title, page, shelf_id=False):
|
|||||||
log.info(u"Shelf {} {}".format(to_save["title"], shelf_action))
|
log.info(u"Shelf {} {}".format(to_save["title"], shelf_action))
|
||||||
flash(flash_text, category="success")
|
flash(flash_text, category="success")
|
||||||
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
|
return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id))
|
||||||
except (OperationalError, InvalidRequestError) as e:
|
except (OperationalError, InvalidRequestError) as ex:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
except Exception as e:
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
|
except Exception as ex:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
log.debug_or_exception(e)
|
log.debug_or_exception(ex)
|
||||||
flash(_(u"There was an error"), category="error")
|
flash(_(u"There was an error"), category="error")
|
||||||
return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page)
|
return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page)
|
||||||
|
|
||||||
@@ -278,6 +286,7 @@ def check_shelf_is_unique(shelf, to_save, shelf_id=False):
|
|||||||
.first() is None
|
.first() is None
|
||||||
|
|
||||||
if not is_shelf_name_unique:
|
if not is_shelf_name_unique:
|
||||||
|
log.error("A public shelf with the name '{}' already exists.".format(to_save["title"]))
|
||||||
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
flash(_(u"A public shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
||||||
category="error")
|
category="error")
|
||||||
else:
|
else:
|
||||||
@@ -288,6 +297,7 @@ def check_shelf_is_unique(shelf, to_save, shelf_id=False):
|
|||||||
.first() is None
|
.first() is None
|
||||||
|
|
||||||
if not is_shelf_name_unique:
|
if not is_shelf_name_unique:
|
||||||
|
log.error("A private shelf with the name '{}' already exists.".format(to_save["title"]))
|
||||||
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
flash(_(u"A private shelf with the name '%(title)s' already exists.", title=to_save["title"]),
|
||||||
category="error")
|
category="error")
|
||||||
return is_shelf_name_unique
|
return is_shelf_name_unique
|
||||||
@@ -311,7 +321,8 @@ def delete_shelf(shelf_id):
|
|||||||
delete_shelf_helper(cur_shelf)
|
delete_shelf_helper(cur_shelf)
|
||||||
except InvalidRequestError:
|
except InvalidRequestError:
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
|
|
||||||
@@ -345,7 +356,8 @@ def order_shelf(shelf_id):
|
|||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
|
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
result = list()
|
result = list()
|
||||||
@@ -415,7 +427,8 @@ def render_show_shelf(shelf_type, shelf_id, page_no, sort_param):
|
|||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
except (OperationalError, InvalidRequestError):
|
except (OperationalError, InvalidRequestError):
|
||||||
ub.session.rollback()
|
ub.session.rollback()
|
||||||
flash(_(u"Settings DB is not Writeable"), category="error")
|
log.error("Settings DB is not Writeable")
|
||||||
|
flash(_("Settings DB is not Writeable"), category="error")
|
||||||
|
|
||||||
return render_title_template(page,
|
return render_title_template(page,
|
||||||
entries=result,
|
entries=result,
|
||||||
|
|||||||
@@ -577,10 +577,6 @@ body.shelforder > div.container-fluid > div.row-fluid > div.col-sm-10:before {
|
|||||||
color: hsla(0, 0%, 100%, .7)
|
color: hsla(0, 0%, 100%, .7)
|
||||||
}
|
}
|
||||||
|
|
||||||
div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > .downloadBtn {
|
|
||||||
border-left: 2px solid rgba(0, 0, 0, .15)
|
|
||||||
}
|
|
||||||
|
|
||||||
div[aria-label="Edit/Delete book"] > .btn {
|
div[aria-label="Edit/Delete book"] > .btn {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
@@ -1502,7 +1498,7 @@ body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form
|
|||||||
}
|
}
|
||||||
|
|
||||||
#books > .cover > a:hover, #books_rand > .cover > a:hover, .book.isotope-item > .cover > a:hover, body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:hover {
|
#books > .cover > a:hover, #books_rand > .cover > a:hover, .book.isotope-item > .cover > a:hover, body > div.container-fluid > div.row-fluid > div.col-sm-10 > div.discover > form > div.col-sm-12 > div.col-sm-12 > div.col-sm-2 > a:hover {
|
||||||
outline: solid var(--color-secondary);
|
/* outline: solid var(--color-secondary); */
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
-o-transition: outline 0s;
|
-o-transition: outline 0s;
|
||||||
transition: outline 0s;
|
transition: outline 0s;
|
||||||
@@ -1783,6 +1779,12 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover {
|
|||||||
margin-top: 0
|
margin-top: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container-fluid .book .meta .series {
|
||||||
|
/* font-weight: 400; */
|
||||||
|
/* font-size: 12px; */
|
||||||
|
color: hsla(0, 0%, 100%, .45);
|
||||||
|
}
|
||||||
|
|
||||||
.container-fluid .book .meta > p {
|
.container-fluid .book .meta > p {
|
||||||
-o-text-overflow: ellipsis;
|
-o-text-overflow: ellipsis;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -2119,8 +2121,7 @@ body > div.container-fluid > div > div.col-sm-10 > div.discover > div.container
|
|||||||
transition: all 0s
|
transition: all 0s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.well > form > .btn {
|
||||||
.book-meta > .bookinfo > .tags .btn-info, .well > form > .btn {
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
-o-transition: background-color .2s, color .2s
|
-o-transition: background-color .2s, color .2s
|
||||||
}
|
}
|
||||||
@@ -2931,8 +2932,9 @@ body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-
|
|||||||
}
|
}
|
||||||
|
|
||||||
#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body > div > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover, body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover {
|
#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body > div > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover, body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover {
|
||||||
margin: 0;
|
margin: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body > div > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover > img, body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover > img {
|
#bookDetailsModal > .modal-dialog.modal-lg > .modal-content > .modal-body > div > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover > img, body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-lg-3.col-xs-5 > div.cover > img {
|
||||||
@@ -2957,46 +2959,35 @@ body > div.container-fluid > div > div.col-sm-10 > div > div > div.col-sm-3.col-
|
|||||||
margin-top: 24px
|
margin-top: 24px
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-meta > .bookinfo > .publishers > span:first-of-type, .book-meta > .bookinfo > .publishing-date > span:first-of-type {
|
.book-meta > .bookinfo > .languages > span:first-of-type,
|
||||||
|
.book-meta > .bookinfo > .publishers > span:first-of-type,
|
||||||
|
.book-meta > .bookinfo > .publishing-date > span:first-of-type,
|
||||||
|
.real_custom_columns > span:first-of-type {
|
||||||
color: hsla(0, 0%, 100%, .45);
|
color: hsla(0, 0%, 100%, .45);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif
|
font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
|
width: 200px;
|
||||||
|
display: inline-block
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-meta > .bookinfo > .publishers > span:last-of-type, .book-meta > .bookinfo > .publishing-date > span:last-of-type {
|
.book-meta > .bookinfo > .languages > span:last-of-type,
|
||||||
|
.book-meta > .bookinfo > .publishers > span:last-of-type,
|
||||||
|
.book-meta > .bookinfo > .publishing-date > span:last-of-type,
|
||||||
|
.real_custom_columns > span:last-of-type {
|
||||||
font-family: Open Sans Semibold, Helvetica Neue, Helvetica, Arial, sans-serif;
|
font-family: Open Sans Semibold, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
-webkit-font-smoothing: antialiased
|
-webkit-font-smoothing: antialiased
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-meta > .bookinfo > .publishers > span:last-of-type {
|
.book-meta > .bookinfo > .languages > span > a,
|
||||||
padding-left: 90px
|
.book-meta > .bookinfo > .publishers > span > a,
|
||||||
|
.book-meta > .bookinfo > .publishing-date > span > a,
|
||||||
|
.real_custom_columns > span > a {
|
||||||
|
color: #fff
|
||||||
}
|
}
|
||||||
|
|
||||||
.real_custom_columns > span:last-of-type {
|
.book-meta > h2, body.book .author {
|
||||||
padding-left: 90px
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-meta > .bookinfo > .publishing-date > span:last-of-type {
|
|
||||||
padding-left: 90px
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-meta > .bookinfo > .languages > span:first-of-type {
|
|
||||||
color: hsla(0, 0%, 100%, .45);
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-meta > .bookinfo > .languages > span:last-of-type {
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: Open Sans Semibold, Helvetica Neue, Helvetica, Arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
color: #fff;
|
|
||||||
padding-left: 85px
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-meta > .bookinfo > .tags .btn-info, .book-meta > h2, body.book .author {
|
|
||||||
font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif
|
font-family: Open Sans Bold, Helvetica Neue, Helvetica, Arial, sans-serif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3077,34 +3068,10 @@ body.book .author {
|
|||||||
background-color: rgba(0, 0, 0, .3)
|
background-color: rgba(0, 0, 0, .3)
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-meta > .bookinfo > .identifiers > p > .btn-success, .book-meta > .bookinfo > .tags .btn-info {
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 2px 3px 0 0;
|
|
||||||
padding: 0 10px
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-meta > .bookinfo > .tags .btn-info {
|
|
||||||
background-color: rgba(0, 0, 0, .15);
|
|
||||||
color: hsla(0, 0%, 100%, .7);
|
|
||||||
font-size: 13px;
|
|
||||||
display: inline-block;
|
|
||||||
border-radius: 4px;
|
|
||||||
-webkit-transition: background-color .2s, color .2s;
|
|
||||||
transition: background-color .2s, color .2s;
|
|
||||||
text-transform: none
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-menu, .tooltip.in {
|
.dropdown-menu, .tooltip.in {
|
||||||
-webkit-transition: opacity .15s ease-out, -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4)
|
-webkit-transition: opacity .15s ease-out, -webkit-transform .15s cubic-bezier(.6, .4, .2, 1.4)
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-meta > .bookinfo > .tags .btn-info:hover {
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: underline
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-meta > .bookinfo > .identifiers, .book-meta > .bookinfo > .tags {
|
.book-meta > .bookinfo > .identifiers, .book-meta > .bookinfo > .tags {
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
margin: 10px 0
|
margin: 10px 0
|
||||||
@@ -3166,7 +3133,7 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
|
|||||||
float: left
|
float: left
|
||||||
}
|
}
|
||||||
|
|
||||||
#add-to-shelf, #btnGroupDrop1, #read-in-browser, #sendbtn, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type, .book-meta .btn-toolbar > .btn-group > .btn-warning, .btn-toolbar > .btn-group > #btnGroupDrop2, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 {
|
#add-to-shelf, #btnGroupDrop1, #readbtn, #sendbtn, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type, .book-meta .btn-toolbar > .btn-group > .btn-warning, .btn-toolbar > .btn-group > #btnGroupDrop2, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 {
|
||||||
background: 0 0;
|
background: 0 0;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
@@ -3180,11 +3147,11 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
|
|||||||
padding-bottom: 5px
|
padding-bottom: 5px
|
||||||
}
|
}
|
||||||
|
|
||||||
#add-to-shelf > span, #btnGroupDrop1 > span, #read-in-browser > span, #sendbtn > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span, .book-meta .btn-toolbar > .btn-group > .btn-warning > span, .btn-toolbar > .btn-group > #btnGroupDrop2 > span, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span {
|
#add-to-shelf > span, #btnGroupDrop1 > span, #readbtn > span, #sendbtn > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span, .book-meta .btn-toolbar > .btn-group > .btn-warning > span, .btn-toolbar > .btn-group > #btnGroupDrop2 > span, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span {
|
||||||
color: hsla(0, 0%, 100%, .7)
|
color: hsla(0, 0%, 100%, .7)
|
||||||
}
|
}
|
||||||
|
|
||||||
#add-to-shelf:hover span, #btnGroupDrop1:hover > span, #read-in-browser:hover > span, #sendbtn:hover > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type:hover > span, .book-meta .btn-toolbar > .btn-group > .btn-warning:hover > span, .btn-toolbar > .btn-group > #btnGroupDrop2:hover > span, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2:hover > span {
|
#add-to-shelf:hover span, #btnGroupDrop1:hover > span, #readbtn:hover > span, #sendbtn:hover > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type:hover > span, .book-meta .btn-toolbar > .btn-group > .btn-warning:hover > span, .btn-toolbar > .btn-group > #btnGroupDrop2:hover > span, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2:hover > span {
|
||||||
color: #fff
|
color: #fff
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3194,13 +3161,13 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] > div.
|
|||||||
font-size: 18px
|
font-size: 18px
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendbtn > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span, .book-meta .btn-toolbar > .btn-group > .btn-warning > span.glyphicon-edit {
|
#sendbtn > span, #readbtn > span, .book-meta .btn-toolbar > .btn-group > .btn-group:nth-child(1) > a:first-of-type > span, .book-meta .btn-toolbar > .btn-group > .btn-warning > span.glyphicon-edit {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 54px;
|
line-height: 54px;
|
||||||
width: 100%
|
width: 100%
|
||||||
}
|
}
|
||||||
|
|
||||||
#read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before {
|
#readbtn > span.glyphicon-book:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-book:before {
|
||||||
content: "\e352";
|
content: "\e352";
|
||||||
font-family: Glyphicons Regular, serif;
|
font-family: Glyphicons Regular, serif;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@@ -3323,7 +3290,9 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
|
|||||||
-ms-transform-origin: center top;
|
-ms-transform-origin: center top;
|
||||||
transform-origin: center top;
|
transform-origin: center top;
|
||||||
border: 0;
|
border: 0;
|
||||||
left: 0 !important
|
left: 0 !important;
|
||||||
|
max-height: 80%;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu > li > a {
|
.dropdown-menu > li > a {
|
||||||
@@ -3345,7 +3314,8 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
|
|||||||
box-shadow: none
|
box-shadow: none
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-meta > .bookinfo > .identifiers > p > .btn-success {
|
.book-meta > .bookinfo .btn-info,
|
||||||
|
.book-meta > .bookinfo .btn-success {
|
||||||
background-color: rgba(0, 0, 0, .15);
|
background-color: rgba(0, 0, 0, .15);
|
||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -3359,11 +3329,21 @@ div.btn-group[role=group][aria-label="Download, send to Kindle, reading"] .dropd
|
|||||||
text-transform: none
|
text-transform: none
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-meta > .bookinfo > .identifiers > p > .btn-success:hover {
|
.book-meta > .bookinfo .btn-info:hover,
|
||||||
|
.book-meta > .bookinfo .btn-success:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.book-meta > .bookinfo .btn-info,
|
||||||
|
.book-meta > .bookinfo .btn-success {
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 2px 3px 0 0;
|
||||||
|
padding: 0 10px
|
||||||
|
}
|
||||||
|
|
||||||
#bookDetailsModal .book-meta {
|
#bookDetailsModal .book-meta {
|
||||||
color: hsla(0, 0%, 100%, .7);
|
color: hsla(0, 0%, 100%, .7);
|
||||||
height: calc(100% - 120px);
|
height: calc(100% - 120px);
|
||||||
@@ -5181,11 +5161,11 @@ body.login > div.navbar.navbar-default.navbar-static-top > div > div.navbar-head
|
|||||||
right: 5px
|
right: 5px
|
||||||
}
|
}
|
||||||
|
|
||||||
#read-in-browser[aria-expanded=true], #shelf-actions > .btn-group.open, .downloadBtn.open, .profileDrop[aria-expanded=true] {
|
#shelf-actions > .btn-group.open, .downloadBtn.open, .profileDrop[aria-expanded=true] {
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
}
|
}
|
||||||
|
|
||||||
#btnGroupDrop1[aria-expanded=true] > span, #read-in-browser[aria-expanded=true] > span, #shelf-actions > .btn-group.open > #add-to-shelf > span {
|
#btnGroupDrop1[aria-expanded=true] > span, #shelf-actions > .btn-group.open > #add-to-shelf > span {
|
||||||
color: #fff
|
color: #fff
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6970,16 +6950,12 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
margin: 45px
|
margin: 45px
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-meta > .bookinfo > .publishing-date > span:last-of-type {
|
.book-meta > .bookinfo > .languages > span:first-of-type,
|
||||||
padding-left: 25px
|
.book-meta > .bookinfo > .publishers > span:first-of-type,
|
||||||
}
|
.book-meta > .bookinfo > .publishing-date > span:first-of-type,
|
||||||
|
.real_custom_columns > span:first-of-type {
|
||||||
.book-meta > .bookinfo > .publishers > span:last-of-type {
|
width: 50%;
|
||||||
padding-left: 70px
|
max-width: 200px;
|
||||||
}
|
|
||||||
|
|
||||||
.book-meta > .bookinfo > .languages > span:last-of-type {
|
|
||||||
padding-left: 65px
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-meta > .bookinfo .publishers {
|
.book-meta > .bookinfo .publishers {
|
||||||
@@ -7506,14 +7482,14 @@ body.edituser.admin > div.container-fluid > div.row-fluid > div.col-sm-10 > div.
|
|||||||
margin-left: 8px
|
margin-left: 8px
|
||||||
}
|
}
|
||||||
|
|
||||||
#read-in-browser > span.caret, .btn-toolbar > .btn-group > #btnGroupDrop2 > span.caret, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.caret {
|
.btn-toolbar > .btn-group > #btnGroupDrop2 > span.caret, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.caret {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
left: 33px;
|
left: 33px;
|
||||||
top: 28px
|
top: 28px
|
||||||
}
|
}
|
||||||
|
|
||||||
#read-in-browser > span.glyphicon-eye-open:before, .btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before {
|
.btn-toolbar > .btn-group > .btn-group > #btnGroupDrop2 > span.glyphicon-eye-open:before {
|
||||||
margin-left: 8px
|
margin-left: 8px
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7914,7 +7890,7 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendbtn2 {
|
#sendbtn2, #read-in-browser {
|
||||||
background: 0 0;
|
background: 0 0;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
@@ -7924,15 +7900,15 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
|
|||||||
padding: 0
|
padding: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendbtn2 > span {
|
#sendbtn2 > span, #read-in-browser > span {
|
||||||
color: hsla(0, 0%, 100%, .7)
|
color: hsla(0, 0%, 100%, .7)
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendbtn2 > span.glyphicon-send:before {
|
#sendbtn2 > span.glyphicon-send:before, #read-in-browser > span.glyphicon-book:before {
|
||||||
margin-left: 8px
|
margin-left: 8px
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendbtn2 > span.caret {
|
#sendbtn2> span.caret, #read-in-browser > span.caret {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
left: 33px;
|
left: 33px;
|
||||||
@@ -7940,11 +7916,11 @@ body.mailset > div.container-fluid > div > div.col-sm-10 > div.discover .glyphic
|
|||||||
padding-bottom: 5px
|
padding-bottom: 5px
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendbtn2:focus span, #sendbtn2:hover span {
|
#sendbtn2:focus span, #sendbtn2:hover span, #read-in-browser:focus span, #read-in-browser:hover span {
|
||||||
color: #fff
|
color: #fff
|
||||||
}
|
}
|
||||||
|
|
||||||
#sendbtn2[aria-expanded=true] {
|
#sendbtn2[aria-expanded=true], #read-in-browser[aria-expanded=true] {
|
||||||
pointer-events: none
|
pointer-events: none
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
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{
|
|
||||||
|
.cover .badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -9,14 +10,15 @@ body.serieslist.grid-view div.container-fluid>div>div.col-sm-10:before{
|
|||||||
background-color: #cc7b19;
|
background-color: #cc7b19;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
box-shadow: 0 0 4px rgba(0,0,0,.6);
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
.cover{
|
|
||||||
box-shadow: 0 0 4px rgba(0,0,0,.6);
|
.cover {
|
||||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover .read{
|
.cover .read {
|
||||||
padding: 0 0px;
|
padding: 0 0;
|
||||||
line-height: 15px;
|
line-height: 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 509 B |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -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 {
|
||||||
@@ -85,15 +84,24 @@ body {
|
|||||||
#progress .bar-load,
|
#progress .bar-load,
|
||||||
#progress .bar-read {
|
#progress .bar-read {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: flex-end;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
transition: width 150ms ease-in-out;
|
transition: width 150ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#progress .from-left {
|
||||||
|
left: 0;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#progress .from-right {
|
||||||
|
right: 0;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
#progress .bar-load {
|
#progress .bar-load {
|
||||||
color: #000;
|
color: #000;
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
@@ -219,3 +227,14 @@ th {
|
|||||||
.dark-theme .overlay {
|
.dark-theme .overlay {
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||||
|
.disabled-scrollbar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide scrollbar for IE, Edge and Firefox */
|
||||||
|
.disabled-scrollbar {
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M8 12a1 1 0 0 1-.707-.293l-5-5a1 1 0 0 1 1.414-1.414L8
|
||||||
|
9.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-5 5A1 1 0 0 1 8 12z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 461 B |
|
Before Width: | Height: | Size: 199 B |
|
Before Width: | Height: | Size: 304 B |
|
Before Width: | Height: | Size: 193 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8 12a1 1 0 0 1-.707-.293l-5-5a1 1 0 0 1 1.414-1.414L8 9.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-5 5A1 1 0 0 1 8 12z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 434 B |
|
Before Width: | Height: | Size: 296 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M13 11a1 1 0 0 1-.707-.293L8 6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414l5-5a1 1 0 0 1 1.414 0l5 5A1 1 0 0 1 13 11z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 458 B |
|
Before Width: | Height: | Size: 193 B |
|
Before Width: | Height: | Size: 296 B |
|
Before Width: | Height: | Size: 199 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M13 11a1 1 0 0 1-.707-.293L8 6.414l-4.293 4.293a1 1 0 0 1-1.414-1.414l5-5a1 1 0 0 1 1.414 0l5 5A1 1 0 0 1 13 11z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 431 B |
|
Before Width: | Height: | Size: 304 B |
@@ -0,0 +1,24 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||||
|
fill="rgba(255,255,255,1)" style="animation:spinLoadingIcon 1s steps(12,end)
|
||||||
|
infinite"><style>@keyframes
|
||||||
|
spinLoadingIcon{to{transform:rotate(360deg)}}</style><path
|
||||||
|
d="M7 3V1s0-1 1-1 1 1 1 1v2s0 1-1 1-1-1-1-1z"/><path d="M4.63
|
||||||
|
4.1l-1-1.73S3.13 1.5 4 1c.87-.5 1.37.37 1.37.37l1 1.73s.5.87-.37
|
||||||
|
1.37c-.87.57-1.37-.37-1.37-.37z" fill-opacity=".93"/><path
|
||||||
|
d="M3.1 6.37l-1.73-1S.5 4.87 1 4c.5-.87 1.37-.37 1.37-.37l1.73 1s.87.5.37
|
||||||
|
1.37c-.5.87-1.37.37-1.37.37z" fill-opacity=".86"/><path d="M3
|
||||||
|
9H1S0 9 0 8s1-1 1-1h2s1 0 1 1-1 1-1 1z" fill-opacity=".79"/><path d="M4.1 11.37l-1.73 1S1.5 12.87 1
|
||||||
|
12c-.5-.87.37-1.37.37-1.37l1.73-1s.87-.5 1.37.37c.5.87-.37 1.37-.37 1.37z"
|
||||||
|
fill-opacity=".72"/><path d="M3.63 13.56l1-1.73s.5-.87
|
||||||
|
1.37-.37c.87.5.37 1.37.37 1.37l-1 1.73s-.5.87-1.37.37c-.87-.5-.37-1.37-.37-1.37z"
|
||||||
|
fill-opacity=".65"/><path d="M7 15v-2s0-1 1-1 1 1 1 1v2s0 1-1
|
||||||
|
1-1-1-1-1z" fill-opacity=".58"/><path d="M10.63
|
||||||
|
14.56l-1-1.73s-.5-.87.37-1.37c.87-.5 1.37.37 1.37.37l1 1.73s.5.87-.37
|
||||||
|
1.37c-.87.5-1.37-.37-1.37-.37z" fill-opacity=".51"/><path
|
||||||
|
d="M13.56 12.37l-1.73-1s-.87-.5-.37-1.37c.5-.87 1.37-.37 1.37-.37l1.73 1s.87.5.37
|
||||||
|
1.37c-.5.87-1.37.37-1.37.37z" fill-opacity=".44"/><path d="M15
|
||||||
|
9h-2s-1 0-1-1 1-1 1-1h2s1 0 1 1-1 1-1 1z" fill-opacity=".37"/><path d="M14.56 5.37l-1.73
|
||||||
|
1s-.87.5-1.37-.37c-.5-.87.37-1.37.37-1.37l1.73-1s.87-.5 1.37.37c.5.87-.37 1.37-.37
|
||||||
|
1.37z" fill-opacity=".3"/><path d="M9.64 3.1l.98-1.66s.5-.874
|
||||||
|
1.37-.37c.87.5.37 1.37.37 1.37l-1 1.73s-.5.87-1.37.37c-.87-.5-.37-1.37-.37-1.37z"
|
||||||
|
fill-opacity=".23"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 16 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" style="animation:spinLoadingIcon 1s steps(12,end) infinite"><style>@keyframes spinLoadingIcon{to{transform:rotate(360deg)}}</style><path d="M7 3V1s0-1 1-1 1 1 1 1v2s0 1-1 1-1-1-1-1z"/><path d="M4.63 4.1l-1-1.73S3.13 1.5 4 1c.87-.5 1.37.37 1.37.37l1 1.73s.5.87-.37 1.37c-.87.57-1.37-.37-1.37-.37z" fill-opacity=".93"/><path d="M3.1 6.37l-1.73-1S.5 4.87 1 4c.5-.87 1.37-.37 1.37-.37l1.73 1s.87.5.37 1.37c-.5.87-1.37.37-1.37.37z" fill-opacity=".86"/><path d="M3 9H1S0 9 0 8s1-1 1-1h2s1 0 1 1-1 1-1 1z" fill-opacity=".79"/><path d="M4.1 11.37l-1.73 1S1.5 12.87 1 12c-.5-.87.37-1.37.37-1.37l1.73-1s.87-.5 1.37.37c.5.87-.37 1.37-.37 1.37z" fill-opacity=".72"/><path d="M3.63 13.56l1-1.73s.5-.87 1.37-.37c.87.5.37 1.37.37 1.37l-1 1.73s-.5.87-1.37.37c-.87-.5-.37-1.37-.37-1.37z" fill-opacity=".65"/><path d="M7 15v-2s0-1 1-1 1 1 1 1v2s0 1-1 1-1-1-1-1z" fill-opacity=".58"/><path d="M10.63 14.56l-1-1.73s-.5-.87.37-1.37c.87-.5 1.37.37 1.37.37l1 1.73s.5.87-.37 1.37c-.87.5-1.37-.37-1.37-.37z" fill-opacity=".51"/><path d="M13.56 12.37l-1.73-1s-.87-.5-.37-1.37c.5-.87 1.37-.37 1.37-.37l1.73 1s.87.5.37 1.37c-.5.87-1.37.37-1.37.37z" fill-opacity=".44"/><path d="M15 9h-2s-1 0-1-1 1-1 1-1h2s1 0 1 1-1 1-1 1z" fill-opacity=".37"/><path d="M14.56 5.37l-1.73 1s-.87.5-1.37-.37c-.5-.87.37-1.37.37-1.37l1.73-1s.87-.5 1.37.37c.5.87-.37 1.37-.37 1.37z" fill-opacity=".3"/><path d="M9.64 3.1l.98-1.66s.5-.874 1.37-.37c.87.5.37 1.37.37 1.37l-1 1.73s-.5.87-1.37.37c-.87-.5-.37-1.37-.37-1.37z" fill-opacity=".23"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
|
||||||
|
16"
|
||||||
|
fill="rgba(255,255,255,1)">
|
||||||
|
<path
|
||||||
|
d="M8 16a8 8 0 1 1 8-8 8.009 8.009 0 0 1-8 8zM8 2a6 6 0 1 0 6 6 6.006 6.006 0 0 0-6-6z">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
d="M8 7a1 1 0 0 0-1 1v3a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1z">
|
||||||
|
</path>
|
||||||
|
<circle
|
||||||
|
cx="8" cy="5" r="1.188">
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 557 B |
|
Before Width: | Height: | Size: 403 B |
@@ -0,0 +1,15 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16
|
||||||
|
16">
|
||||||
|
<path
|
||||||
|
d="M8 16a8 8 0 1 1 8-8 8.009 8.009 0 0 1-8 8zM8 2a6 6 0 1 0 6 6 6.006 6.006 0 0 0-6-6z">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
d="M8 7a1 1 0 0 0-1 1v3a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1z">
|
||||||
|
</path>
|
||||||
|
<circle
|
||||||
|
cx="8" cy="5" r="1.188">
|
||||||
|
</circle>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 530 B |
|
Before Width: | Height: | Size: 933 B |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M13 13c-.3 0-.5-.1-.7-.3L8 8.4l-4.3 4.3c-.9.9-2.3-.5-1.4-1.4l5-5c.4-.4 1-.4 1.4 0l5 5c.6.6.2 1.7-.7 1.7zm0-11H3C1.7 2 1.7 4 3 4h10c1.3 0 1.3-2 0-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 255 B |
|
Before Width: | Height: | Size: 179 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13 13c-.3 0-.5-.1-.7-.3L8 8.4l-4.3 4.3c-.9.9-2.3-.5-1.4-1.4l5-5c.4-.4 1-.4 1.4 0l5 5c.6.6.2 1.7-.7 1.7zm0-11H3C1.7 2 1.7 4 3 4h10c1.3 0 1.3-2 0-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 266 B |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M15 3.7V13c0 1.5-1.53 3-3 3H7.13c-.72 0-1.63-.5-2.13-1l-5-5s.84-1 .87-1c.13-.1.33-.2.53-.2.1 0 .3.1.4.2L4 10.6V2.7c0-.6.4-1 1-1s1 .4 1 1v4.6h1V1c0-.6.4-1 1-1s1 .4 1 1v6.3h1V1.7c0-.6.4-1 1-1s1 .4 1 1v5.7h1V3.7c0-.6.4-1 1-1s1 .4 1 1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 339 B |
|
Before Width: | Height: | Size: 301 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M15 3.7V13c0 1.5-1.53 3-3 3H7.13c-.72 0-1.63-.5-2.13-1l-5-5s.84-1 .87-1c.13-.1.33-.2.53-.2.1 0 .3.1.4.2L4 10.6V2.7c0-.6.4-1 1-1s1 .4 1 1v4.6h1V1c0-.6.4-1 1-1s1 .4 1 1v6.3h1V1.7c0-.6.4-1 1-1s1 .4 1 1v5.7h1V3.7c0-.6.4-1 1-1s1 .4 1 1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 312 B |
|
Before Width: | Height: | Size: 583 B |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M8 10c-.3 0-.5-.1-.7-.3l-5-5c-.9-.9.5-2.3 1.4-1.4L8 7.6l4.3-4.3c.9-.9 2.3.5 1.4 1.4l-5 5c-.2.2-.4.3-.7.3zm5 2H3c-1.3 0-1.3 2 0 2h10c1.3 0 1.3-2 0-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 256 B |
|
Before Width: | Height: | Size: 175 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M8 10c-.3 0-.5-.1-.7-.3l-5-5c-.9-.9.5-2.3 1.4-1.4L8 7.6l4.3-4.3c.9-.9 2.3.5 1.4 1.4l-5 5c-.2.2-.4.3-.7.3zm5 2H3c-1.3 0-1.3 2 0 2h10c1.3 0 1.3-2 0-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 229 B |
|
Before Width: | Height: | Size: 276 B |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M1 1a1 1 0 011 1v2.4A7 7 0 118 15a7 7 0 01-4.9-2 1 1 0 011.4-1.5 5 5 0 10-1-5.5H6a1 1 0 010 2H1a1 1 0 01-1-1V2a1 1 0 011-1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 231 B |
|
Before Width: | Height: | Size: 360 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M1 1a1 1 0 011 1v2.4A7 7 0 118 15a7 7 0 01-4.9-2 1 1 0 011.4-1.5 5 5 0 10-1-5.5H6a1 1 0 010 2H1a1 1 0 01-1-1V2a1 1 0 011-1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 204 B |
|
Before Width: | Height: | Size: 731 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M15 1a1 1 0 0 0-1 1v2.418A6.995 6.995 0 1 0 8 15a6.954 6.954 0 0 0 4.95-2.05 1 1 0 0 0-1.414-1.414A5.019 5.019 0 1 1 12.549 6H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 521 B |
|
Before Width: | Height: | Size: 359 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M15 1a1 1 0 0 0-1 1v2.418A6.995 6.995 0 1 0 8 15a6.954 6.954 0 0 0 4.95-2.05 1 1 0 0 0-1.414-1.414A5.019 5.019 0 1 1 12.549 6H10a1 1 0 0 0 0 2h5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 494 B |
|
Before Width: | Height: | Size: 714 B |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"/></svg>
|
||||||
|
After Width: | Height: | Size: 302 B |
|
Before Width: | Height: | Size: 218 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M0 4h1.5c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5H0zM9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM16 4h-1.5c-1 0-1.5.5-1.5 1.5v5c0 1 .5 1.5 1.5 1.5H16z"/></svg>
|
||||||
|
After Width: | Height: | Size: 275 B |
|
Before Width: | Height: | Size: 332 B |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 228 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M9.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C5 4.5 5.5 4 6.5 4zM11 0v.5c0 1-.5 1.5-1.5 1.5h-3C5.5 2 5 1.5 5 .5V0h6zM11 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 280 B |
|
Before Width: | Height: | Size: 349 B |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 509 B |
|
Before Width: | Height: | Size: 297 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M5.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5C1 4.5 1.5 4 2.5 4zM7 0v.5C7 1.5 6.5 2 5.5 2h-3C1.5 2 1 1.5 1 .5V0h6zM7 16v-.5c0-1-.5-1.5-1.5-1.5h-3c-1 0-1.5.5-1.5 1.5v.5h6zM13.5 4c1 0 1.5.5 1.5 1.5v5c0 1-.5 1.5-1.5 1.5h-3c-1 0-1.5-.5-1.5-1.5v-5c0-1 .5-1.5 1.5-1.5zM15 0v.5c0 1-.5 1.5-1.5 1.5h-3C9.5 2 9 1.5 9 .5V0h6zM15 16v-.507c0-1-.5-1.5-1.5-1.5h-3C9.5 14 9 14.5 9 15.5v.5h6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 482 B |
|
Before Width: | Height: | Size: 490 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M12.408 8.217l-8.083-6.7A.2.2 0 0 0 4 1.672V12.3a.2.2 0 0 0 .333.146l2.56-2.372 1.857 3.9A1.125 1.125 0 1 0 10.782 13L8.913 9.075l3.4-.51a.2.2 0 0 0 .095-.348z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 505 B |
|
Before Width: | Height: | Size: 461 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M12.408 8.217l-8.083-6.7A.2.2 0 0 0 4 1.672V12.3a.2.2 0 0 0 .333.146l2.56-2.372 1.857 3.9A1.125 1.125 0 1 0 10.782 13L8.913 9.075l3.4-.51a.2.2 0 0 0 .095-.348z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 478 B |
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,2 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||||
|
fill="rgba(255,255,255,1)"><path d="M1.5 3.5C.5 3.5 0 4 0 5v6.5c0 1 .5 1.5 1.5 1.5h4c1 0 1.5-.5 1.5-1.5V5c0-1-.5-1.5-1.5-1.5zm2 1.2c.8 0 1.4.2 1.8.6.5.4.7 1 .7 1.7 0 .5-.2 1-.5 1.4-.2.3-.5.7-1 1l-.6.4c-.4.3-.6.4-.75.56-.15.14-.25.24-.35.44H6v1.3H1c0-.6.1-1.1.3-1.5.3-.6.7-1 1.5-1.6.7-.4 1.1-.8 1.28-1 .32-.3.42-.6.42-1 0-.3-.1-.6-.23-.8-.17-.2-.37-.3-.77-.3s-.7.1-.9.5c-.04.2-.1.5-.1.9H1.1c0-.6.1-1.1.3-1.5.4-.7 1.1-1.1 2.1-1.1zM10.54 3.54C9.5 3.54 9 4 9 5v6.5c0 1 .5 1.5 1.54 1.5h4c.96 0 1.46-.5 1.46-1.5V5c0-1-.5-1.46-1.5-1.46zm1.9.95c.7 0 1.3.2 1.7.5.4.4.6.8.6 1.4 0 .4-.1.8-.4 1.1-.2.2-.3.3-.5.4.1 0 .3.1.6.3.4.3.5.8.5 1.4 0 .6-.2 1.2-.6 1.6-.4.5-1.1.7-1.9.7-1 0-1.8-.3-2.2-1-.14-.29-.24-.69-.24-1.29h1.4c0 .3 0 .5.1.7.2.4.5.5 1 .5.3 0 .5-.1.7-.3.2-.2.3-.5.3-.8 0-.5-.2-.8-.6-.95-.2-.05-.5-.15-1-.15v-1c.5 0 .8-.1 1-.14.3-.1.5-.4.5-.9 0-.3-.1-.5-.2-.7-.2-.2-.4-.3-.7-.3-.3 0-.6.1-.75.3-.2.2-.2.5-.2.86h-1.34c0-.4.1-.7.19-1.1 0-.12.2-.32.4-.62.2-.2.4-.3.7-.4.3-.1.6-.1 1-.1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 347 B |