Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0e0422010 | ||
|
|
01ab75a158 | ||
|
|
d9c10b830a | ||
|
|
35f6f4c727 | ||
|
|
3b216bfa07 | ||
|
|
e8e2f789e5 | ||
|
|
bbef41290f | ||
|
|
81b85445d8 | ||
|
|
35209ede67 | ||
|
|
0c0313f375 | ||
|
|
6bf0753978 | ||
|
|
a02f621f08 | ||
|
|
de1bc3f9af | ||
|
|
01090169a7 | ||
|
|
b564a97cdf | ||
|
|
a118fffc99 | ||
|
|
5b59aab81a | ||
|
|
7d3d0c661e | ||
|
|
f6f20ebc77 | ||
|
|
58cb54c76f | ||
|
|
3fc326fa48 | ||
|
|
a50aff67a2 | ||
|
|
3c1f5fd37f | ||
|
|
8ae066c387 | ||
|
|
0feb62c142 | ||
|
|
96b1e8960b | ||
|
|
df67079573 | ||
|
|
e3dbf7a88d | ||
|
|
8dd0585919 | ||
|
|
c830a5936e | ||
|
|
405f3c181f | ||
|
|
6d839d5cc7 | ||
|
|
bbadfa2251 | ||
|
|
7b8b2f93a0 | ||
|
|
c1030dfd13 | ||
|
|
a90177afa0 | ||
|
|
c095ee3c14 | ||
|
|
ae1f515446 | ||
|
|
0548fbb685 | ||
|
|
f22e4d996c | ||
|
|
3e0d8763c3 | ||
|
|
47f5e2ffb4 | ||
|
|
f39dc100b4 | ||
|
|
573c9f9fb4 | ||
|
|
785726deee | ||
|
|
4edd1914b4 | ||
|
|
222929e741 | ||
|
|
70b67077cc | ||
|
|
ec73558b03 | ||
|
|
bdedec90dd | ||
|
|
d45085215f | ||
|
|
b85627da5c | ||
|
|
592288cb22 | ||
|
|
2e3a3ee460 | ||
|
|
b7927a0df1 | ||
|
|
021298374e | ||
|
|
92f65882b2 | ||
|
|
f0399d04b7 | ||
|
|
9b57fa25de | ||
|
|
afbe77de3d | ||
|
|
d26d357151 | ||
|
|
4db9691cfc | ||
|
|
45c433caab | ||
|
|
25422b3411 | ||
|
|
a184f4e71a | ||
|
|
4569188008 | ||
|
|
9d9acb058d | ||
|
|
7d67168a4a | ||
|
|
09751d8b87 | ||
|
|
fd5ab0ef53 | ||
|
|
3bf173d958 | ||
|
|
98d630d453 | ||
|
|
eb2e816bfd | ||
|
|
bd01e840ca | ||
|
|
91a21ababe | ||
|
|
f4096b136e | ||
|
|
7598dfe952 | ||
|
|
5ed3b1cf53 | ||
|
|
7640ac1b3b | ||
|
|
66874f8163 | ||
|
|
3f91313303 | ||
|
|
8438b2a07b | ||
|
|
67e3721530 | ||
|
|
2252d661c0 | ||
|
|
87e526642c | ||
|
|
7f9da94a18 | ||
|
|
ec7c2db971 | ||
|
|
fcd2b68359 | ||
|
|
a1d372630d | ||
|
|
fc859afb92 | ||
|
|
2e0d0a2429 | ||
|
|
d084a06e63 | ||
|
|
e880238cb9 | ||
|
|
f58c5bee1c | ||
|
|
cbb9edac19 | ||
|
|
1b8bd27b3c | ||
|
|
6cb713d62c | ||
|
|
642af2f973 | ||
|
|
9273843062 | ||
|
|
2989586c6d | ||
|
|
5ede079401 | ||
|
|
7ad419dc8c | ||
|
|
bcdc976414 | ||
|
|
6aad9378b8 | ||
|
|
6f5390ead5 | ||
|
|
d624b67e93 | ||
|
|
01cc97c1b2 | ||
|
|
7c016265d2 | ||
|
|
974549b1af | ||
|
|
add502d236 | ||
|
|
294c594cbb | ||
|
|
60aa016734 | ||
|
|
27e8fbd248 | ||
|
|
d856c4d78e | ||
|
|
25b09a532f | ||
|
|
c1f4ca36b6 | ||
|
|
58379159fb | ||
|
|
17470b3b56 | ||
|
|
42cc13d1e2 | ||
|
|
ecc5cb167e | ||
|
|
d72210c6ae | ||
|
|
61deda1076 | ||
|
|
1e0ff0f9c2 | ||
|
|
00bf1f5ec9 | ||
|
|
2b6f5b1565 | ||
|
|
e5abc5f281 | ||
|
|
95371d0d7f | ||
|
|
c7df8a1a34 | ||
|
|
b414d91964 | ||
|
|
82164fc8e7 | ||
|
|
a5415e00d5 | ||
|
|
434029270b | ||
|
|
b3b85bf692 | ||
|
|
a36c6da3ae | ||
|
|
9e72c3b40d | ||
|
|
a0cebfbfb1 | ||
|
|
64e833f5d6 | ||
|
|
99ec99b539 | ||
|
|
75c68d92ec | ||
|
|
27f71d910b | ||
|
|
88278672d8 |
@@ -31,7 +31,7 @@ If applicable, add screenshots to help explain your problem.
|
||||
- OS: [e.g. Windows 10/Raspberry Pi OS]
|
||||
- Python version: [e.g. python2.7]
|
||||
- Calibre-Web version: [e.g. 0.6.8 or 087c4c59 (git rev-parse --short HEAD)]:
|
||||
- Docker container: [None/Technosoft2000/LinuxServer]:
|
||||
- Docker container: [None/LinuxServer]:
|
||||
- Special Hardware: [e.g. Rasperry Pi Zero]
|
||||
- Browser: [e.g. Chrome 83.0.4103.97, Safari 13.3.7, Firefox 68.0.1 ESR]
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
||||
- full graphical setup
|
||||
- User management with fine-grained per-user permissions
|
||||
- Admin interface
|
||||
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
|
||||
- User Interface in brazilian, czech, dutch, english, finnish, french, german, greek, hungarian, italian, japanese, khmer, korean, polish, russian, simplified and traditional chinese, spanish, swedish, turkish, ukrainian
|
||||
- OPDS feed for eBook reader apps
|
||||
- Filter and search by titles, authors, tags, series and language
|
||||
- Create a custom book collection (shelves)
|
||||
@@ -37,9 +37,9 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
||||
- "Magic Link" login to make it easy to log on eReaders
|
||||
- Login via LDAP, google/github oauth and via proxy authentication
|
||||
|
||||
## Quick start
|
||||
## Installation
|
||||
|
||||
#### Install via pip
|
||||
#### Installation via pip (recommended)
|
||||
1. Install calibre web via pip with the command `pip install calibreweb` (Depending on your OS and or distro the command could also be `pip3`).
|
||||
2. Optional features can also be installed via pip, please refer to [this page](https://github.com/janeczku/calibre-web/wiki/Dependencies-in-Calibre-Web-Linux-Windows) for details
|
||||
3. Calibre-Web can be started afterwards by typing `cps` or `python3 -m cps`
|
||||
@@ -47,18 +47,21 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
|
||||
#### Manual installation
|
||||
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x). Alternativly set up a python virtual environment.
|
||||
2. Execute the command: `python3 cps.py` (or `nohup python3 cps.py` - recommended if you want to exit the terminal window)
|
||||
|
||||
|
||||
Issues with Ubuntu:
|
||||
Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead.
|
||||
|
||||
## Quick start
|
||||
|
||||
Point your browser to `http://localhost:8083` or `http://localhost:8083/opds` for the OPDS catalog
|
||||
Set `Location of Calibre database` to the path of the folder where your Calibre library (metadata.db) lives, push "submit" button\
|
||||
Optionally a Google Drive can be used to host the calibre library [-> Using Google Drive integration](https://github.com/janeczku/calibre-web/wiki/Configuration#using-google-drive-integration)
|
||||
Go to Login page
|
||||
|
||||
**Default admin login:**\
|
||||
#### Default admin login:
|
||||
*Username:* admin\
|
||||
*Password:* admin123
|
||||
|
||||
**Issues with Ubuntu:**
|
||||
Please note that running the above install command can fail on some versions of Ubuntu, saying `"can't combine user with prefix"`. This is a [known bug](https://github.com/pypa/pip/issues/3826) and can be remedied by using the command `pip install --system --target vendor -r requirements.txt` instead.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
+25
-15
@@ -10,20 +10,30 @@ To receive fixes for security vulnerabilities it is required to always upgrade t
|
||||
|
||||
## History
|
||||
|
||||
| Fixed in | Description |CVE number |
|
||||
| ---------- |---------|---------|
|
||||
| 3rd July 2018 | Guest access acts as a backdoor||
|
||||
| V 0.6.7 |Hardcoded secret key for sessions |CVE-2020-12627 |
|
||||
| V 0.6.13|Calibre-Web Metadata cross site scripting |CVE-2021-25964|
|
||||
| V 0.6.13|Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo||
|
||||
| V 0.6.13|JavaScript could get executed in the description field. Thanks to @ranjit-git ||
|
||||
| V 0.6.13|JavaScript could get executed in a custom column of type "comment" field ||
|
||||
| V 0.6.13|JavaScript could get executed after converting a book to another format with a title containing javascript code||
|
||||
| V 0.6.13|JavaScript could get executed after converting a book to another format with a username containing javascript code||
|
||||
| V 0.6.13|JavaScript could get executed in the description series, categories or publishers title||
|
||||
| V 0.6.13|JavaScript could get executed in the shelf title||
|
||||
| V 0.6.13|Login with the old session cookie after logout. Thanks to @ibarrionuevo||
|
||||
| V 0.6.14|CSRF was possible. Thanks to @mik317 ||
|
||||
| V 0.6.14|Cross-Site Scripting vulnerability on typeahead inputs. Thanks to @notdodo||
|
||||
| Fixed in | Description |CVE number |
|
||||
|---------------|--------------------------------------------------------------------------------------------------------------------|---------|
|
||||
| 3rd July 2018 | Guest access acts as a backdoor ||
|
||||
| V 0.6.7 | Hardcoded secret key for sessions |CVE-2020-12627 |
|
||||
| V 0.6.13 | Calibre-Web Metadata cross site scripting |CVE-2021-25964|
|
||||
| V 0.6.13 | Name of Shelves are only visible to users who can access the corresponding shelf Thanks to @ibarrionuevo ||
|
||||
| V 0.6.13 | JavaScript could get executed in the description field. Thanks to @ranjit-git and Hagai Wechsler (WhiteSource) ||
|
||||
| V 0.6.13 | JavaScript could get executed in a custom column of type "comment" field ||
|
||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a title containing javascript code ||
|
||||
| V 0.6.13 | JavaScript could get executed after converting a book to another format with a username containing javascript code ||
|
||||
| V 0.6.13 | JavaScript could get executed in the description series, categories or publishers title ||
|
||||
| V 0.6.13 | JavaScript could get executed in the shelf title ||
|
||||
| V 0.6.13 | Login with the old session cookie after logout. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.14 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
|
||||
| V 0.6.14 | Migrated some routes to POST-requests (CSRF protection). Thanks to @scara31 ||
|
||||
| V 0.6.15 | Fix for "javascript:" script links in identifier. Thanks to @scara31 ||
|
||||
| V 0.6.15 | Cross-Site Scripting vulnerability on uploaded cover file names. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.15 | Creating public shelfs is now denied if user is missing the edit public shelf right. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.15 | Changed error message in case of trying to delete a shelf unauthorized. Thanks to @ibarrionuevo ||
|
||||
| V 0.6.16 | JavaScript could get executed on authors page. Thanks to @alicaz ||
|
||||
| V 0.6.16 | Localhost can no longer be used to upload covers. Thanks to @scara31 ||
|
||||
| V 0.6.16 | Another case where public shelfs could be created without permission is prevented. Thanks to @ibarrionuevo ||
|
||||
|
||||
|
||||
## Staement regarding Log4j (CVE-2021-44228 and related)
|
||||
|
||||
Calibre-web is not affected by bugs related to Log4j. Calibre-Web is a python program, therefore not using Java, and not using the Java logging feature log4j.
|
||||
|
||||
+8
-1
@@ -19,6 +19,7 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
__package__ = "cps"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -35,6 +36,7 @@ from flask_principal import Principal
|
||||
from . import config_sql, logger, cache_buster, cli, ub, db
|
||||
from .reverseproxy import ReverseProxied
|
||||
from .server import WebServer
|
||||
from .dep_check import dependency_check
|
||||
|
||||
try:
|
||||
import lxml
|
||||
@@ -100,6 +102,7 @@ _BABEL_TRANSLATIONS = set()
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
from . import services
|
||||
|
||||
db.CalibreDB.update_config(config)
|
||||
@@ -126,7 +129,11 @@ def create_app():
|
||||
print('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
|
||||
web_server.stop(True)
|
||||
sys.exit(7)
|
||||
|
||||
for res in dependency_check() + dependency_check(True):
|
||||
log.info('*** "{}" version does not fit the requirements. Should: {}, Found: {}, please consider installing required version ***'
|
||||
.format(res['name'],
|
||||
res['target'],
|
||||
res['found']))
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
|
||||
if os.environ.get('FLASK_DEBUG'):
|
||||
|
||||
+59
-33
@@ -33,8 +33,9 @@ try:
|
||||
except ImportError:
|
||||
flaskwtf_version = _(u'not installed')
|
||||
|
||||
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants
|
||||
from . import db, calibre_db, converter, uploader, server, isoLanguages, constants, gdriveutils, dep_check
|
||||
from .render_template import render_title_template
|
||||
|
||||
try:
|
||||
from flask_login import __version__ as flask_loginVersion
|
||||
except ImportError:
|
||||
@@ -67,38 +68,65 @@ from . import services
|
||||
|
||||
about = flask.Blueprint('about', __name__)
|
||||
|
||||
ret = dict()
|
||||
req = dep_check.load_dependencys(False)
|
||||
opt = dep_check.load_dependencys(True)
|
||||
for i in (req + opt):
|
||||
ret[i[1]] = i[0]
|
||||
|
||||
_VERSIONS = OrderedDict(
|
||||
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
|
||||
Python=sys.version,
|
||||
Calibre_Web=constants.STABLE_VERSION['version'] + ' - '
|
||||
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
|
||||
+ constants.NIGHTLY_VERSION[1].replace('%','%%'),
|
||||
WebServer=server.VERSION,
|
||||
Flask=flask.__version__,
|
||||
Flask_Login=flask_loginVersion,
|
||||
Flask_Principal=flask_principal.__version__,
|
||||
Flask_WTF=flaskwtf_version,
|
||||
Werkzeug=werkzeug.__version__,
|
||||
Babel=babel.__version__,
|
||||
Jinja2=jinja2.__version__,
|
||||
Requests=requests.__version__,
|
||||
SqlAlchemy=sqlalchemy.__version__,
|
||||
pySqlite=sqlite3.version,
|
||||
SQLite=sqlite3.sqlite_version,
|
||||
iso639=isoLanguages.__version__,
|
||||
pytz=pytz.__version__,
|
||||
Unidecode=unidecode_version,
|
||||
Scholarly=scholarly_version,
|
||||
Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
|
||||
python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
|
||||
Goodreads=u'installed' if bool(services.goodreads_support) else None,
|
||||
jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
||||
flask_dance=flask_danceVersion,
|
||||
greenlet=greenlet_Version
|
||||
)
|
||||
_VERSIONS.update(uploader.get_versions())
|
||||
if constants.NIGHTLY_VERSION[0] == "$Format:%H$":
|
||||
calibre_web_version = constants.STABLE_VERSION['version']
|
||||
else:
|
||||
calibre_web_version = (constants.STABLE_VERSION['version'] + ' - '
|
||||
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
|
||||
+ constants.NIGHTLY_VERSION[1].replace('%','%%'))
|
||||
if getattr(sys, 'frozen', False):
|
||||
calibre_web_version += " - Exe-Version"
|
||||
elif constants.HOME_CONFIG:
|
||||
calibre_web_version += " - pyPi"
|
||||
|
||||
if not ret:
|
||||
_VERSIONS = OrderedDict(
|
||||
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
|
||||
Python=sys.version,
|
||||
Calibre_Web=calibre_web_version,
|
||||
WebServer=server.VERSION,
|
||||
Flask=flask.__version__,
|
||||
Flask_Login=flask_loginVersion,
|
||||
Flask_Principal=flask_principal.__version__,
|
||||
Flask_WTF=flaskwtf_version,
|
||||
Werkzeug=werkzeug.__version__,
|
||||
Babel=babel.__version__,
|
||||
Jinja2=jinja2.__version__,
|
||||
Requests=requests.__version__,
|
||||
SqlAlchemy=sqlalchemy.__version__,
|
||||
pySqlite=sqlite3.version,
|
||||
SQLite=sqlite3.sqlite_version,
|
||||
iso639=isoLanguages.__version__,
|
||||
pytz=pytz.__version__,
|
||||
Unidecode=unidecode_version,
|
||||
Scholarly=scholarly_version,
|
||||
Flask_SimpleLDAP=u'installed' if bool(services.ldap) else None,
|
||||
python_LDAP=services.ldapVersion if bool(services.ldapVersion) else None,
|
||||
Goodreads=u'installed' if bool(services.goodreads_support) else None,
|
||||
jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
|
||||
flask_dance=flask_danceVersion,
|
||||
greenlet=greenlet_Version
|
||||
)
|
||||
_VERSIONS.update(gdriveutils.get_versions())
|
||||
_VERSIONS.update(uploader.get_versions(True))
|
||||
else:
|
||||
_VERSIONS = OrderedDict(
|
||||
Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
|
||||
Python = sys.version,
|
||||
Calibre_Web=calibre_web_version,
|
||||
Werkzeug = werkzeug.__version__,
|
||||
Jinja2=jinja2.__version__,
|
||||
pySqlite = sqlite3.version,
|
||||
SQLite = sqlite3.sqlite_version,
|
||||
)
|
||||
_VERSIONS.update(ret)
|
||||
_VERSIONS.update(uploader.get_versions(False))
|
||||
|
||||
def collect_stats():
|
||||
_VERSIONS['ebook converter'] = _(converter.get_calibre_version())
|
||||
@@ -115,5 +143,3 @@ def stats():
|
||||
series = calibre_db.session.query(db.Series).count()
|
||||
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
|
||||
categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat")
|
||||
|
||||
|
||||
|
||||
+61
-17
@@ -129,11 +129,11 @@ def admin_forbidden():
|
||||
abort(403)
|
||||
|
||||
|
||||
@admi.route("/shutdown")
|
||||
@admi.route("/shutdown", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def shutdown():
|
||||
task = int(request.args.get("parameter").strip())
|
||||
task = request.get_json().get('parameter', -1)
|
||||
showtext = {}
|
||||
if task in (0, 1): # valid commandos received
|
||||
# close all database connections
|
||||
@@ -590,6 +590,8 @@ def load_dialogtexts(element_id):
|
||||
texts["main"] = _('Are you sure you want to change shelf sync behavior for the selected user(s)?')
|
||||
elif element_id == "db_submit":
|
||||
texts["main"] = _('Are you sure you want to change Calibre library location?')
|
||||
elif element_id == "btnfullsync":
|
||||
texts["main"] = _("Are you sure you want delete Calibre-Web's sync database to force a full sync with your Kobo Reader?")
|
||||
return json.dumps(texts)
|
||||
|
||||
|
||||
@@ -754,7 +756,12 @@ def prepare_tags(user, action, tags_name, id_list):
|
||||
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>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def add_user_0_restriction(res_type):
|
||||
return add_restriction(res_type, 0)
|
||||
|
||||
@admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -801,7 +808,13 @@ def add_restriction(res_type, user_id):
|
||||
return ""
|
||||
|
||||
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>", defaults={"user_id": 0}, methods=['POST'])
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def delete_user_0_restriction(res_type):
|
||||
return delete_restriction(res_type, 0)
|
||||
|
||||
|
||||
@admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -889,10 +902,18 @@ def list_restriction(res_type, user_id):
|
||||
else:
|
||||
json_dumps = ""
|
||||
js = json.dumps(json_dumps)
|
||||
response = make_response(js) #.replace("'", '"')
|
||||
response = make_response(js)
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
|
||||
@admi.route("/ajax/fullsync", methods=["POST"])
|
||||
@login_required
|
||||
def ajax_fullsync():
|
||||
count = ub.session.query(ub.KoboSyncedBooks).filter(current_user.id == ub.KoboSyncedBooks.user_id).delete()
|
||||
message = _("{} sync entries deleted").format(count)
|
||||
ub.session_commit(message)
|
||||
return Response(json.dumps([{"type": "success", "message": message}]), mimetype='application/json')
|
||||
|
||||
|
||||
@admi.route("/ajax/pathchooser/")
|
||||
@login_required
|
||||
@@ -1186,11 +1207,20 @@ def _db_configuration_update_helper():
|
||||
if not calibre_db.setup_db(to_save['config_calibre_dir'], ub.app_DB_path):
|
||||
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
|
||||
gdrive_error)
|
||||
# if db changed -> delete shelfs, delete download books, delete read books, kobo sync...
|
||||
ub.session.query(ub.Downloads).delete()
|
||||
ub.session.query(ub.ArchivedBook).delete()
|
||||
ub.session.query(ub.ReadBook).delete()
|
||||
ub.session.query(ub.BookShelf).delete()
|
||||
ub.session.query(ub.Bookmark).delete()
|
||||
ub.session.query(ub.KoboReadingState).delete()
|
||||
ub.session.query(ub.KoboStatistics).delete()
|
||||
ub.session.query(ub.KoboSyncedBooks).delete()
|
||||
ub.session_commit()
|
||||
_config_string(to_save, "config_calibre_dir")
|
||||
calibre_db.update_config(config)
|
||||
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
|
||||
flash(_(u"DB is not Writeable"), category="warning")
|
||||
# warning = {'type': "warning", 'message': _(u"DB is not Writeable")}
|
||||
config.save()
|
||||
return _db_configuration_result(None, gdrive_error)
|
||||
|
||||
@@ -1348,7 +1378,9 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support):
|
||||
raise Exception(_(u"E-mail is not from valid domain"))
|
||||
except Exception as ex:
|
||||
flash(str(ex), 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,
|
||||
config=config,
|
||||
translations=translations,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||
try:
|
||||
@@ -1383,16 +1415,25 @@ def _delete_user(content):
|
||||
for us in ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id):
|
||||
ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete()
|
||||
ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id).delete()
|
||||
ub.session.query(ub.Bookmark).filter(content.id == ub.Bookmark.user_id).delete()
|
||||
ub.session.query(ub.User).filter(ub.User.id == content.id).delete()
|
||||
ub.session.query(ub.ArchivedBook).filter(ub.ArchivedBook.user_id == content.id).delete()
|
||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == content.id).delete()
|
||||
ub.session.query(ub.User_Sessions).filter(ub.User_Sessions.user_id == content.id).delete()
|
||||
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == content.id).delete()
|
||||
# delete KoboReadingState and all it's children
|
||||
kobo_entries = ub.session.query(ub.KoboReadingState).filter(ub.KoboReadingState.user_id == content.id).all()
|
||||
for kobo_entry in kobo_entries:
|
||||
ub.session.delete(kobo_entry)
|
||||
ub.session_commit()
|
||||
log.info(u"User {} deleted".format(content.name))
|
||||
return(_(u"User '%(nick)s' deleted", nick=content.name))
|
||||
log.info("User {} deleted".format(content.name))
|
||||
return(_("User '%(nick)s' deleted", nick=content.name))
|
||||
else:
|
||||
log.warning(_(u"Can't delete Guest User"))
|
||||
raise Exception(_(u"Can't delete Guest User"))
|
||||
log.warning(_("Can't delete Guest User"))
|
||||
raise Exception(_("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"))
|
||||
log.warning("No admin user remaining, can't delete user")
|
||||
raise Exception(_("No admin user remaining, can't delete user"))
|
||||
|
||||
|
||||
def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
||||
@@ -1463,6 +1504,7 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support):
|
||||
kobo_support=kobo_support,
|
||||
new_user=0,
|
||||
content=content,
|
||||
config=config,
|
||||
registered_oauth=oauth_check,
|
||||
title=_(u"Edit User %(nick)s", nick=content.name),
|
||||
page="edituser")
|
||||
@@ -1496,7 +1538,8 @@ def new_user():
|
||||
content.sidebar_view = config.config_default_show
|
||||
content.locale = config.config_default_locale
|
||||
content.default_language = config.config_default_language
|
||||
return render_title_template("user_edit.html", new_user=1, content=content, translations=translations,
|
||||
return render_title_template("user_edit.html", new_user=1, content=content,
|
||||
config=config, translations=translations,
|
||||
languages=languages, title=_(u"Add new user"), page="newuser",
|
||||
kobo_support=kobo_support, registered_oauth=oauth_check)
|
||||
|
||||
@@ -1571,7 +1614,7 @@ def edit_user(user_id):
|
||||
if not content or (not config.config_anonbrowse and content.name == "Guest"):
|
||||
flash(_(u"User not found"), category="error")
|
||||
return redirect(url_for('admin.admin'))
|
||||
languages = calibre_db.speaking_language()
|
||||
languages = calibre_db.speaking_language(return_all_languages=True)
|
||||
translations = babel.list_translations() + [LC('en')]
|
||||
kobo_support = feature_support['kobo'] and config.config_kobo_sync
|
||||
if request.method == "POST":
|
||||
@@ -1584,6 +1627,7 @@ def edit_user(user_id):
|
||||
languages=languages,
|
||||
new_user=0,
|
||||
content=content,
|
||||
config=config,
|
||||
registered_oauth=oauth_check,
|
||||
mail_configured=config.get_mail_server_configured(),
|
||||
kobo_support=kobo_support,
|
||||
@@ -1591,7 +1635,7 @@ def edit_user(user_id):
|
||||
page="edituser")
|
||||
|
||||
|
||||
@admi.route("/admin/resetpassword/<int:user_id>")
|
||||
@admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def reset_user_password(user_id):
|
||||
@@ -1767,7 +1811,7 @@ def ldap_import_create_user(user, user_data):
|
||||
return 0, message
|
||||
|
||||
|
||||
@admi.route('/import_ldap_users')
|
||||
@admi.route('/import_ldap_users', methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def import_ldap_users():
|
||||
|
||||
@@ -45,6 +45,7 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
|
||||
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
|
||||
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
|
||||
parser.add_argument('-f', action='store_true', help='Flag is depreciated and will be removed in next version')
|
||||
parser.add_argument('-l', action='store_true', help='Allow loading covers from localhost')
|
||||
args = parser.parse_args()
|
||||
|
||||
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db")
|
||||
@@ -77,6 +78,8 @@ if (args.k and not args.c) or (not args.k and args.c):
|
||||
if args.k == "":
|
||||
keyfilepath = ""
|
||||
|
||||
# load covers from localhost
|
||||
allow_localhost = args.l or None
|
||||
# handle and check ip address argument
|
||||
ip_address = args.i or None
|
||||
if ip_address:
|
||||
|
||||
+14
-14
@@ -56,25 +56,25 @@ COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
|
||||
|
||||
def _cover_processing(tmp_file_name, img, extension):
|
||||
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg')
|
||||
if use_IM:
|
||||
# convert to jpg because calibre only supports jpg
|
||||
if extension in NO_JPEG_EXTENSIONS:
|
||||
with Image(filename=tmp_file_name) as imgc:
|
||||
if extension in NO_JPEG_EXTENSIONS:
|
||||
if use_IM:
|
||||
with Image(blob=img) as imgc:
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace('rgb')
|
||||
imgc.save(tmp_cover_name)
|
||||
imgc.save(filename=tmp_cover_name)
|
||||
return tmp_cover_name
|
||||
|
||||
if not img:
|
||||
else:
|
||||
return None
|
||||
if img:
|
||||
with open(tmp_cover_name, 'wb') as f:
|
||||
f.write(img)
|
||||
return tmp_cover_name
|
||||
else:
|
||||
return None
|
||||
|
||||
with open(tmp_cover_name, 'wb') as f:
|
||||
f.write(img)
|
||||
return tmp_cover_name
|
||||
|
||||
|
||||
def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable):
|
||||
cover_data = None
|
||||
cover_data = extension = None
|
||||
if original_file_extension.upper() == '.CBZ':
|
||||
cf = zipfile.ZipFile(tmp_file_name)
|
||||
for name in cf.namelist():
|
||||
@@ -106,7 +106,7 @@ def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecu
|
||||
break
|
||||
except Exception as ex:
|
||||
log.debug('Rarfile failed with error: %s', ex)
|
||||
return cover_data
|
||||
return cover_data, extension
|
||||
|
||||
|
||||
def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||
@@ -121,7 +121,7 @@ def _extractCover(tmp_file_name, original_file_extension, rarExecutable):
|
||||
cover_data = archive.getPage(index)
|
||||
break
|
||||
else:
|
||||
cover_data = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
|
||||
cover_data, extension = _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable)
|
||||
return _cover_processing(tmp_file_name, cover_data, extension)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ def selected_roles(dictionary):
|
||||
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
|
||||
'series_id, languages, publisher')
|
||||
|
||||
STABLE_VERSION = {'version': '0.6.14'}
|
||||
STABLE_VERSION = {'version': '0.6.16'}
|
||||
|
||||
NIGHTLY_VERSION = {}
|
||||
NIGHTLY_VERSION[0] = '$Format:%H$'
|
||||
|
||||
@@ -23,6 +23,7 @@ import re
|
||||
import ast
|
||||
import json
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint
|
||||
@@ -166,6 +167,8 @@ class Identifiers(Base):
|
||||
return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
|
||||
elif format_type == "isfdb":
|
||||
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
|
||||
elif self.val.lower().startswith("javascript:"):
|
||||
return quote(self.val)
|
||||
else:
|
||||
return u"{0}".format(self.val)
|
||||
|
||||
@@ -174,8 +177,8 @@ class Comments(Base):
|
||||
__tablename__ = 'comments'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
book = Column(Integer, ForeignKey('books.id'), nullable=False, unique=True)
|
||||
text = Column(String(collation='NOCASE'), nullable=False)
|
||||
book = Column(Integer, ForeignKey('books.id'), nullable=False)
|
||||
|
||||
def __init__(self, text, book):
|
||||
self.text = text
|
||||
@@ -550,11 +553,8 @@ class CalibreDB():
|
||||
|
||||
@classmethod
|
||||
def setup_db(cls, config_calibre_dir, app_db_path):
|
||||
# cls.config = config
|
||||
cls.dispose()
|
||||
|
||||
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
|
||||
|
||||
if not config_calibre_dir:
|
||||
cls.config.invalidate()
|
||||
return False
|
||||
@@ -612,7 +612,7 @@ class CalibreDB():
|
||||
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
|
||||
def common_filters(self, allow_show_archived=False):
|
||||
def common_filters(self, allow_show_archived=False, return_all_languages=False):
|
||||
if not allow_show_archived:
|
||||
archived_books = (
|
||||
ub.session.query(ub.ArchivedBook)
|
||||
@@ -625,10 +625,10 @@ class CalibreDB():
|
||||
else:
|
||||
archived_filter = true()
|
||||
|
||||
if current_user.filter_language() != "all":
|
||||
lang_filter = Books.languages.any(Languages.lang_code == current_user.filter_language())
|
||||
else:
|
||||
if current_user.filter_language() == "all" or return_all_languages:
|
||||
lang_filter = true()
|
||||
else:
|
||||
lang_filter = Books.languages.any(Languages.lang_code == current_user.filter_language())
|
||||
negtags_list = current_user.list_denied_tags()
|
||||
postags_list = current_user.list_allowed_tags()
|
||||
neg_content_tags_filter = false() if negtags_list == [''] else Books.tags.any(Tags.name.in_(negtags_list))
|
||||
@@ -780,7 +780,7 @@ class CalibreDB():
|
||||
|
||||
# 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]
|
||||
order = order[0] if order else [Books.sort]
|
||||
pagination = None
|
||||
result = self.search_query(term, *join).order_by(*order).all()
|
||||
result_count = len(result)
|
||||
@@ -796,27 +796,29 @@ class CalibreDB():
|
||||
return result[offset:limit_all], result_count, pagination
|
||||
|
||||
# Creates for all stored languages a translated speaking name in the array for the UI
|
||||
def speaking_language(self, languages=None):
|
||||
def speaking_language(self, languages=None, return_all_languages=False, with_count=False, reverse_order=False):
|
||||
from . import get_locale
|
||||
|
||||
if not languages:
|
||||
languages = self.session.query(Languages) \
|
||||
.join(books_languages_link) \
|
||||
.join(Books) \
|
||||
.filter(self.common_filters()) \
|
||||
.group_by(text('books_languages_link.lang_code')).all()
|
||||
for lang in languages:
|
||||
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||
#try:
|
||||
# if lang.lang_code.lower() == "und":
|
||||
# lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||
# # lang.name = _("Undetermined")
|
||||
# else:
|
||||
# cur_l = LC.parse(lang.lang_code)
|
||||
# lang.name = cur_l.get_language_name(get_locale())
|
||||
#except UnknownLocaleError:
|
||||
# lang.name = _(isoLanguages.get(part3=lang.lang_code).name)
|
||||
return languages
|
||||
if with_count:
|
||||
if not languages:
|
||||
languages = self.session.query(Languages, func.count('books_languages_link.book'))\
|
||||
.join(books_languages_link).join(Books)\
|
||||
.filter(self.common_filters(return_all_languages=return_all_languages)) \
|
||||
.group_by(text('books_languages_link.lang_code')).all()
|
||||
for lang in languages:
|
||||
lang[0].name = isoLanguages.get_language_name(get_locale(), lang[0].lang_code)
|
||||
return sorted(languages, key=lambda x: x[0].name, reverse=reverse_order)
|
||||
else:
|
||||
if not languages:
|
||||
languages = self.session.query(Languages) \
|
||||
.join(books_languages_link) \
|
||||
.join(Books) \
|
||||
.filter(self.common_filters(return_all_languages=return_all_languages)) \
|
||||
.group_by(text('books_languages_link.lang_code')).all()
|
||||
for lang in languages:
|
||||
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
|
||||
return sorted(languages, key=lambda x: x.name, reverse=reverse_order)
|
||||
|
||||
|
||||
def update_title_sort(self, config, conn=None):
|
||||
# user defined sort function for calibre databases (Series, etc.)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from .constants import BASE_DIR
|
||||
try:
|
||||
from importlib_metadata import version
|
||||
importlib = True
|
||||
ImportNotFound = BaseException
|
||||
except ImportError:
|
||||
importlib = False
|
||||
|
||||
|
||||
if not importlib:
|
||||
try:
|
||||
import pkg_resources
|
||||
from pkg_resources import DistributionNotFound as ImportNotFound
|
||||
pkgresources = True
|
||||
except ImportError as e:
|
||||
pkgresources = False
|
||||
|
||||
def load_dependencys(optional=False):
|
||||
deps = list()
|
||||
if importlib or pkgresources:
|
||||
if optional:
|
||||
req_path = os.path.join(BASE_DIR, "optional-requirements.txt")
|
||||
else:
|
||||
req_path = os.path.join(BASE_DIR, "requirements.txt")
|
||||
if os.path.exists(req_path):
|
||||
with open(req_path, 'r') as f:
|
||||
for line in f:
|
||||
if not line.startswith('#') and not line == '\n' and not line.startswith('git'):
|
||||
res = re.match(r'(.*?)([<=>\s]+)([\d\.]+),?\s?([<=>\s]+)?([\d\.]+)?', line.strip())
|
||||
try:
|
||||
if importlib:
|
||||
dep_version = version(res.group(1))
|
||||
else:
|
||||
dep_version = pkg_resources.get_distribution(res.group(1)).version
|
||||
except ImportNotFound:
|
||||
if optional:
|
||||
continue
|
||||
dep_version = "not installed"
|
||||
deps.append([dep_version, res.group(1), res.group(2), res.group(3), res.group(4), res.group(5)])
|
||||
return deps
|
||||
|
||||
|
||||
def dependency_check(optional=False):
|
||||
d = list()
|
||||
deps = load_dependencys(optional)
|
||||
for dep in deps:
|
||||
try:
|
||||
dep_version_int = [int(x) for x in dep[0].split('.')]
|
||||
low_check = [int(x) for x in dep[3].split('.')]
|
||||
high_check = [int(x) for x in dep[5].split('.')]
|
||||
except AttributeError:
|
||||
high_check = None
|
||||
except ValueError:
|
||||
d.append({'name': dep[1],
|
||||
'target': "available",
|
||||
'found': "Not available"
|
||||
})
|
||||
continue
|
||||
|
||||
if dep[2].strip() == "==":
|
||||
if dep_version_int != low_check:
|
||||
d.append({'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[2] + dep[3]})
|
||||
continue
|
||||
elif dep[2].strip() == ">=":
|
||||
if dep_version_int < low_check:
|
||||
d.append({'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[2] + dep[3]})
|
||||
continue
|
||||
elif dep[2].strip() == ">":
|
||||
if dep_version_int <= low_check:
|
||||
d.append({'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[2] + dep[3]})
|
||||
continue
|
||||
if dep[4] and dep[5]:
|
||||
if dep[4].strip() == "<":
|
||||
if dep_version_int >= high_check:
|
||||
d.append(
|
||||
{'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[4] + dep[5]})
|
||||
continue
|
||||
elif dep[4].strip() == "<=":
|
||||
if dep_version_int > high_check:
|
||||
d.append(
|
||||
{'name': dep[1],
|
||||
'found': dep[0],
|
||||
"target": dep[4] + dep[5]})
|
||||
continue
|
||||
return d
|
||||
Regular → Executable
+37
-24
@@ -26,12 +26,13 @@ import json
|
||||
from shutil import copyfile
|
||||
from uuid import uuid4
|
||||
from markupsafe import escape
|
||||
from functools import wraps
|
||||
|
||||
try:
|
||||
from lxml.html.clean import clean_html
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# Improve this to check if scholarly is available in a global way, like other pythonic libraries
|
||||
try:
|
||||
from scholarly import scholarly
|
||||
@@ -52,13 +53,6 @@ from .tasks.upload import TaskUpload
|
||||
from .render_template import render_title_template
|
||||
from .usermanagement import login_required_if_no_ano
|
||||
|
||||
try:
|
||||
from functools import wraps
|
||||
except ImportError:
|
||||
pass # We're not using Python 3
|
||||
|
||||
|
||||
|
||||
|
||||
editbook = Blueprint('editbook', __name__)
|
||||
log = logger.create()
|
||||
@@ -67,7 +61,7 @@ log = logger.create()
|
||||
def upload_required(f):
|
||||
@wraps(f)
|
||||
def inner(*args, **kwargs):
|
||||
if current_user.role_upload() or current_user.role_admin():
|
||||
if current_user.role_upload():
|
||||
return f(*args, **kwargs)
|
||||
abort(403)
|
||||
|
||||
@@ -238,17 +232,17 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
|
||||
changed = True
|
||||
return changed, error
|
||||
|
||||
@editbook.route("/ajax/delete/<int:book_id>")
|
||||
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_book_from_details(book_id):
|
||||
return Response(delete_book(book_id, "", True), mimetype='application/json')
|
||||
return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
|
||||
|
||||
|
||||
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""})
|
||||
@editbook.route("/delete/<int:book_id>/<string:book_format>")
|
||||
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
|
||||
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_book_ajax(book_id, book_format):
|
||||
return delete_book(book_id, book_format, False)
|
||||
return delete_book_from_table(book_id, book_format, False)
|
||||
|
||||
|
||||
def delete_whole_book(book_id, book):
|
||||
@@ -318,7 +312,7 @@ def render_delete_book_result(book_format, jsonResponse, warning, book_id):
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
|
||||
def delete_book(book_id, book_format, jsonResponse):
|
||||
def delete_book_from_table(book_id, book_format, jsonResponse):
|
||||
warning = {}
|
||||
if current_user.role_delete_books():
|
||||
book = calibre_db.get_book(book_id)
|
||||
@@ -347,6 +341,8 @@ def delete_book(book_id, book_format, jsonResponse):
|
||||
else:
|
||||
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
|
||||
filter(db.Data.format == book_format).delete()
|
||||
if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']:
|
||||
kobo_sync_status.remove_synced_book(book.id, True)
|
||||
calibre_db.session.commit()
|
||||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
@@ -363,7 +359,16 @@ def delete_book(book_id, book_format, jsonResponse):
|
||||
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)
|
||||
return render_delete_book_result(book_format, jsonResponse, warning, book_id)
|
||||
message = _("You are missing permissions to delete books")
|
||||
if jsonResponse:
|
||||
return json.dumps({"location": url_for("editbook.edit_book", book_id=book_id),
|
||||
"type": "danger",
|
||||
"format": "",
|
||||
"message": message})
|
||||
else:
|
||||
flash(message, category="error")
|
||||
return redirect(url_for('editbook.edit_book', book_id=book_id))
|
||||
|
||||
|
||||
def render_edit_book(book_id):
|
||||
@@ -454,7 +459,7 @@ def edit_book_series_index(series_index, book):
|
||||
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 str(book.series_index) != series_index:
|
||||
book.series_index = series_index
|
||||
modif_date = True
|
||||
return modif_date
|
||||
@@ -484,11 +489,11 @@ def edit_book_languages(languages, book, upload=False, invalid=None):
|
||||
else:
|
||||
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, 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")
|
||||
raise ValueError(_(u"'%(langname)s' is not a valid language", langname=l))
|
||||
# ToDo: Not working correct
|
||||
if upload and len(input_l) == 1:
|
||||
# If the language of the file is excluded from the users view, it's not imported, to allow the user to view
|
||||
@@ -503,7 +508,7 @@ def edit_book_languages(languages, book, upload=False, invalid=None):
|
||||
|
||||
def edit_book_publisher(publishers, book):
|
||||
changed = False
|
||||
if publishers:
|
||||
if publishers:
|
||||
publisher = publishers.rstrip().strip()
|
||||
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,
|
||||
@@ -833,7 +838,7 @@ def edit_book(book_id):
|
||||
|
||||
if modif_date:
|
||||
book.last_modified = datetime.utcnow()
|
||||
kobo_sync_status.remove_synced_book(edited_books_id)
|
||||
kobo_sync_status.remove_synced_book(edited_books_id, all=True)
|
||||
|
||||
calibre_db.session.merge(book)
|
||||
calibre_db.session.commit()
|
||||
@@ -848,6 +853,10 @@ def edit_book(book_id):
|
||||
calibre_db.session.rollback()
|
||||
flash(error, category="error")
|
||||
return render_edit_book(book_id)
|
||||
except ValueError as e:
|
||||
calibre_db.session.rollback()
|
||||
flash(str(e), category="error")
|
||||
return redirect(url_for('web.show_book', book_id=book.id))
|
||||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
calibre_db.session.rollback()
|
||||
@@ -944,7 +953,11 @@ def create_book_on_upload(modif_date, meta):
|
||||
modif_date |= edit_book_series_index(meta.series_id, db_book)
|
||||
|
||||
# add languages
|
||||
modif_date |= edit_book_languages(meta.languages, db_book, upload=True)
|
||||
invalid=[]
|
||||
modif_date |= edit_book_languages(meta.languages, db_book, upload=True, invalid=invalid)
|
||||
if invalid:
|
||||
for l in invalid:
|
||||
flash(_(u"'%(langname)s' is not a valid language", langname=l), category="warning")
|
||||
|
||||
# handle tags
|
||||
modif_date |= edit_book_tags(meta.tags, db_book)
|
||||
@@ -1007,7 +1020,7 @@ def move_coverfile(meta, db_book):
|
||||
category="error")
|
||||
|
||||
|
||||
@editbook.route("/upload", methods=["GET", "POST"])
|
||||
@editbook.route("/upload", methods=["POST"])
|
||||
@login_required_if_no_ano
|
||||
@upload_required
|
||||
def upload():
|
||||
@@ -1247,7 +1260,7 @@ def merge_list_book():
|
||||
element.format,
|
||||
element.uncompressed_size,
|
||||
to_name))
|
||||
delete_book(from_book.id,"", True)
|
||||
delete_book_from_table(from_book.id,"", True)
|
||||
return json.dumps({'success': True})
|
||||
return ""
|
||||
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ def revoke_watch_gdrive():
|
||||
try:
|
||||
gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'],
|
||||
last_watch_response['resourceId'])
|
||||
except HttpError:
|
||||
except (HttpError, AttributeError):
|
||||
pass
|
||||
config.config_google_drive_watch_changes_response = {}
|
||||
config.save()
|
||||
|
||||
@@ -35,6 +35,15 @@ except ImportError:
|
||||
from sqlalchemy.exc import OperationalError, InvalidRequestError
|
||||
from sqlalchemy.sql.expression import text
|
||||
|
||||
try:
|
||||
from six import __version__ as six_version
|
||||
except ImportError:
|
||||
six_version = "not installed"
|
||||
try:
|
||||
from httplib2 import __version__ as httplib2_version
|
||||
except ImportError:
|
||||
httplib2_version = "not installed"
|
||||
|
||||
try:
|
||||
from apiclient import errors
|
||||
from httplib2 import ServerNotFoundError
|
||||
@@ -47,11 +56,13 @@ try:
|
||||
from pydrive2.auth import GoogleAuth
|
||||
from pydrive2.drive import GoogleDrive
|
||||
from pydrive2.auth import RefreshError
|
||||
from pydrive2.files import ApiRequestError
|
||||
except ImportError as err:
|
||||
try:
|
||||
from pydrive.auth import GoogleAuth
|
||||
from pydrive.drive import GoogleDrive
|
||||
from pydrive.auth import RefreshError
|
||||
from pydrive.files import ApiRequestError
|
||||
except ImportError as err:
|
||||
importError = err
|
||||
gdrive_support = False
|
||||
@@ -313,6 +324,11 @@ def getFolderId(path, drive):
|
||||
log.error("gdrive.db DB is not Writeable")
|
||||
log.debug('Database error: %s', ex)
|
||||
session.rollback()
|
||||
except ApiRequestError as ex:
|
||||
log.error('{} {}'.format(ex.error['message'], path))
|
||||
session.rollback()
|
||||
except RefreshError as ex:
|
||||
log.error(ex)
|
||||
return currentFolderId
|
||||
|
||||
|
||||
@@ -659,3 +675,8 @@ def get_error_text(client_secrets=None):
|
||||
return 'Callback url (redirect url) is missing in client_secrets.json'
|
||||
if client_secrets:
|
||||
client_secrets.update(filedata['web'])
|
||||
|
||||
|
||||
def get_versions():
|
||||
return {'six': six_version,
|
||||
'httplib2': httplib2_version}
|
||||
|
||||
+29
-22
@@ -17,18 +17,18 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import io
|
||||
import mimetypes
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import socket
|
||||
import unicodedata
|
||||
from datetime import datetime, timedelta
|
||||
from tempfile import gettempdir
|
||||
|
||||
from urllib.parse import urlparse
|
||||
import requests
|
||||
|
||||
from babel.dates import format_datetime
|
||||
from babel.units import format_unit
|
||||
from flask import send_from_directory, make_response, redirect, abort, url_for
|
||||
@@ -38,11 +38,7 @@ from sqlalchemy.sql.expression import true, false, and_, text, func
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.security import generate_password_hash
|
||||
from markupsafe import escape
|
||||
|
||||
try:
|
||||
from urllib.parse import quote
|
||||
except ImportError:
|
||||
from urllib import quote
|
||||
from urllib.parse import quote
|
||||
|
||||
try:
|
||||
import unidecode
|
||||
@@ -50,7 +46,7 @@ try:
|
||||
except ImportError:
|
||||
use_unidecode = False
|
||||
|
||||
from . import calibre_db
|
||||
from . import calibre_db, cli
|
||||
from .tasks.convert import TaskConvert
|
||||
from . import logger, config, get_locale, db, ub
|
||||
from . import gdriveutils as gd
|
||||
@@ -63,7 +59,7 @@ log = logger.create()
|
||||
|
||||
try:
|
||||
from wand.image import Image
|
||||
from wand.exceptions import MissingDelegateError
|
||||
from wand.exceptions import MissingDelegateError, BlobError
|
||||
use_IM = True
|
||||
except (ImportError, RuntimeError) as e:
|
||||
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
|
||||
@@ -233,7 +229,7 @@ def get_valid_filename(value, replace_whitespace=True):
|
||||
value = value[:-1]+u'_'
|
||||
value = value.replace("/", "_").replace(":", "_").strip('\0')
|
||||
if use_unidecode:
|
||||
if not config.config_unicode_filename:
|
||||
if config.config_unicode_filename:
|
||||
value = (unidecode.unidecode(value))
|
||||
else:
|
||||
value = value.replace(u'§', u'SS')
|
||||
@@ -588,10 +584,17 @@ def get_book_cover_internal(book, use_generic_cover_on_failure):
|
||||
# saves book cover from url
|
||||
def save_cover_from_url(url, book_path):
|
||||
try:
|
||||
if not cli.allow_localhost:
|
||||
# 127.0.x.x, localhost, [::1], [::ffff:7f00:1]
|
||||
ip = socket.getaddrinfo(urlparse(url).hostname, 0)[0][4][0]
|
||||
if ip.startswith("127.") or ip.startswith('::ffff:7f') or ip == "::1":
|
||||
log.error("Localhost was accessed for cover upload")
|
||||
return False, _("You are not allowed to access localhost for cover uploads")
|
||||
img = requests.get(url, timeout=(10, 200)) # ToDo: Error Handling
|
||||
img.raise_for_status()
|
||||
return save_cover(img, book_path)
|
||||
except (requests.exceptions.HTTPError,
|
||||
except (socket.gaierror,
|
||||
requests.exceptions.HTTPError,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout) as ex:
|
||||
log.info(u'Cover Download Error %s', ex)
|
||||
@@ -638,13 +641,17 @@ def save_cover(img, book_path):
|
||||
return False, _("Only jpg/jpeg/png/webp/bmp files are supported as coverfile")
|
||||
# convert to jpg because calibre only supports jpg
|
||||
if content_type != 'image/jpg':
|
||||
if hasattr(img, 'stream'):
|
||||
imgc = Image(blob=img.stream)
|
||||
else:
|
||||
imgc = Image(blob=io.BytesIO(img.content))
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace("rgb")
|
||||
img = imgc
|
||||
try:
|
||||
if hasattr(img, 'stream'):
|
||||
imgc = Image(blob=img.stream)
|
||||
else:
|
||||
imgc = Image(blob=io.BytesIO(img.content))
|
||||
imgc.format = 'jpeg'
|
||||
imgc.transform_colorspace("rgb")
|
||||
img = imgc
|
||||
except (BlobError, MissingDelegateError):
|
||||
log.error("Invalid cover file content")
|
||||
return False, _("Invalid cover file content")
|
||||
else:
|
||||
if content_type not in 'image/jpeg':
|
||||
log.error("Only jpg/jpeg files are supported as coverfile")
|
||||
@@ -669,9 +676,9 @@ def save_cover(img, book_path):
|
||||
|
||||
def do_download_file(book, book_format, client, data, headers):
|
||||
if config.config_use_google_drive:
|
||||
startTime = time.time()
|
||||
#startTime = time.time()
|
||||
df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format)
|
||||
log.debug('%s', time.time() - startTime)
|
||||
#log.debug('%s', time.time() - startTime)
|
||||
if df:
|
||||
return gd.do_gdrive_download(df, headers)
|
||||
else:
|
||||
@@ -819,7 +826,7 @@ def get_cc_columns(filter_config_custom_read=False):
|
||||
|
||||
def get_download_link(book_id, book_format, client):
|
||||
book_format = book_format.split(".")[0]
|
||||
book = calibre_db.get_filtered_book(book_id)
|
||||
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)
|
||||
if book:
|
||||
data1 = calibre_db.get_book_format(book.id, book_format.upper())
|
||||
else:
|
||||
|
||||
+8
-1
@@ -17,6 +17,9 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
|
||||
from . import logger
|
||||
|
||||
log = logger.create()
|
||||
|
||||
|
||||
try:
|
||||
@@ -50,7 +53,11 @@ def get_language_names(locale):
|
||||
|
||||
|
||||
def get_language_name(locale, lang_code):
|
||||
return get_language_names(locale)[lang_code]
|
||||
try:
|
||||
return get_language_names(locale)[lang_code]
|
||||
except KeyError:
|
||||
log.error('Missing translation for language name: {}'.format(lang_code))
|
||||
return "Unknown"
|
||||
|
||||
|
||||
def get_language_codes(locale, language_names, remainder=None):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+79
-49
@@ -22,11 +22,8 @@ import datetime
|
||||
import os
|
||||
import uuid
|
||||
from time import gmtime, strftime
|
||||
|
||||
try:
|
||||
from urllib import unquote
|
||||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
import json
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
@@ -102,6 +99,8 @@ def make_request_to_kobo_store(sync_token=None):
|
||||
allow_redirects=False,
|
||||
timeout=(2, 10)
|
||||
)
|
||||
log.debug("Content: " + str(store_response.content))
|
||||
log.debug("StatusCode: " + str(store_response.status_code))
|
||||
return store_response
|
||||
|
||||
|
||||
@@ -110,7 +109,8 @@ def redirect_or_proxy_request():
|
||||
if request.method == "GET":
|
||||
return redirect(get_store_url_for_current_request(), 307)
|
||||
else:
|
||||
# The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves.
|
||||
# The Kobo device turns other request types into GET requests on redirects,
|
||||
# so we instead proxy to the Kobo store ourselves.
|
||||
store_response = make_request_to_kobo_store()
|
||||
|
||||
response_headers = store_response.headers
|
||||
@@ -142,12 +142,16 @@ def HandleSyncRequest():
|
||||
if not current_app.wsgi_app.is_proxied:
|
||||
log.debug('Kobo: Received unproxied request, changed request port to external server port')
|
||||
|
||||
# TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header
|
||||
# instead so that the device triggers another sync.
|
||||
# if no books synced don't respect sync_token
|
||||
if not ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == current_user.id).count():
|
||||
sync_token.books_last_modified = datetime.datetime.min
|
||||
sync_token.books_last_created = datetime.datetime.min
|
||||
sync_token.reading_state_last_modified = datetime.datetime.min
|
||||
|
||||
new_books_last_modified = sync_token.books_last_modified
|
||||
new_books_last_created = sync_token.books_last_created
|
||||
new_books_last_modified = sync_token.books_last_modified # needed for sync selected shelfs only
|
||||
new_books_last_created = sync_token.books_last_created # needed to distinguish between new and changed entitlement
|
||||
new_reading_state_last_modified = sync_token.reading_state_last_modified
|
||||
|
||||
new_archived_last_modified = datetime.datetime.min
|
||||
sync_results = []
|
||||
|
||||
@@ -169,10 +173,10 @@ def HandleSyncRequest():
|
||||
ub.BookShelf.date_added,
|
||||
ub.ArchivedBook.is_archived)
|
||||
changed_entries = (changed_entries
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
|
||||
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
|
||||
ub.KoboSyncedBooks.book_id == None))
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
||||
ub.ArchivedBook.user_id == current_user.id))
|
||||
.filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
|
||||
.filter(ub.KoboSyncedBooks.user_id == current_user.id)))
|
||||
.filter(ub.BookShelf.date_added > sync_token.books_last_modified)
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.filter(calibre_db.common_filters(allow_show_archived=True))
|
||||
@@ -192,11 +196,11 @@ def HandleSyncRequest():
|
||||
ub.ArchivedBook.last_modified,
|
||||
ub.ArchivedBook.is_archived)
|
||||
changed_entries = (changed_entries
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id)
|
||||
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True)
|
||||
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id,
|
||||
ub.KoboSyncedBooks.book_id == None))
|
||||
.filter(calibre_db.common_filters())
|
||||
.join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
|
||||
ub.ArchivedBook.user_id == current_user.id))
|
||||
.filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
|
||||
.filter(ub.KoboSyncedBooks.user_id == current_user.id)))
|
||||
.filter(calibre_db.common_filters(allow_show_archived=True))
|
||||
.filter(db.Data.format.in_(KOBO_FORMATS))
|
||||
.order_by(db.Books.last_modified)
|
||||
.order_by(db.Books.id)
|
||||
@@ -208,8 +212,8 @@ def HandleSyncRequest():
|
||||
books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
|
||||
else:
|
||||
books = changed_entries.limit(SYNC_ITEM_LIMIT)
|
||||
log.debug("Books to Sync: {}".format(len(books.all())))
|
||||
for book in books:
|
||||
kobo_sync_status.add_synced_books(book.Books.id)
|
||||
formats = [data.format for data in book.Books.data]
|
||||
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats:
|
||||
helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
|
||||
@@ -248,14 +252,17 @@ def HandleSyncRequest():
|
||||
pass
|
||||
|
||||
new_books_last_created = max(ts_created, new_books_last_created)
|
||||
kobo_sync_status.add_synced_books(book.Books.id)
|
||||
|
||||
if sqlalchemy_version2:
|
||||
max_change = calibre_db.session.execute(changed_entries
|
||||
.filter(ub.ArchivedBook.is_archived)
|
||||
.filter(ub.ArchivedBook.user_id == current_user.id)
|
||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
|
||||
.columns(db.Books).first()
|
||||
else:
|
||||
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived) \
|
||||
max_change = changed_entries.from_self().filter(ub.ArchivedBook.is_archived)\
|
||||
.filter(ub.ArchivedBook.user_id==current_user.id) \
|
||||
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
|
||||
|
||||
max_change = max_change.last_modified if max_change else new_archived_last_modified
|
||||
@@ -290,7 +297,8 @@ def HandleSyncRequest():
|
||||
|
||||
changed_reading_states = changed_reading_states.filter(
|
||||
and_(ub.KoboReadingState.user_id == current_user.id,
|
||||
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))
|
||||
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements)))\
|
||||
.order_by(ub.KoboReadingState.last_modified)
|
||||
cont_sync |= bool(changed_reading_states.count() > SYNC_ITEM_LIMIT)
|
||||
for kobo_reading_state in changed_reading_states.limit(SYNC_ITEM_LIMIT).all():
|
||||
book = calibre_db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
|
||||
@@ -304,18 +312,19 @@ def HandleSyncRequest():
|
||||
|
||||
sync_shelves(sync_token, sync_results, only_kobo_shelves)
|
||||
|
||||
sync_token.books_last_created = new_books_last_created
|
||||
# update last created timestamp to distinguish between new and changed entitlements
|
||||
if not cont_sync:
|
||||
sync_token.books_last_created = new_books_last_created
|
||||
sync_token.books_last_modified = new_books_last_modified
|
||||
sync_token.archive_last_modified = new_archived_last_modified
|
||||
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
||||
# sync_token.books_last_id = books_last_id
|
||||
|
||||
return generate_sync_response(sync_token, sync_results, cont_sync)
|
||||
|
||||
|
||||
def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||
extra_headers = {}
|
||||
if config.config_kobo_proxy:
|
||||
if config.config_kobo_proxy and not set_cont:
|
||||
# Merge in sync results from the official Kobo store.
|
||||
try:
|
||||
store_response = make_request_to_kobo_store(sync_token)
|
||||
@@ -334,8 +343,9 @@ def generate_sync_response(sync_token, sync_results, set_cont=False):
|
||||
sync_token.to_headers(extra_headers)
|
||||
|
||||
# log.debug("Kobo Sync Content: {}".format(sync_results))
|
||||
response = make_response(jsonify(sync_results), extra_headers)
|
||||
|
||||
# jsonify decodes the unicode string different to what kobo expects
|
||||
response = make_response(json.dumps(sync_results), extra_headers)
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
|
||||
|
||||
@@ -352,7 +362,9 @@ def HandleMetadataRequest(book_uuid):
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
metadata = get_metadata(book)
|
||||
return jsonify([metadata])
|
||||
response = make_response(json.dumps([metadata], ensure_ascii=False))
|
||||
response.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
return response
|
||||
|
||||
|
||||
def get_download_url_for_book(book, book_format):
|
||||
@@ -380,7 +392,7 @@ def get_download_url_for_book(book, book_format):
|
||||
|
||||
|
||||
def create_book_entitlement(book, archived):
|
||||
book_uuid = book.uuid
|
||||
book_uuid = str(book.uuid)
|
||||
return {
|
||||
"Accessibility": "Full",
|
||||
"ActivePeriod": {"From": convert_to_kobo_timestamp_string(datetime.datetime.now())},
|
||||
@@ -407,18 +419,15 @@ def get_description(book):
|
||||
return book.comments[0].text
|
||||
|
||||
|
||||
# TODO handle multiple authors
|
||||
def get_author(book):
|
||||
if not book.authors:
|
||||
return {"Contributors": None}
|
||||
if len(book.authors) > 1:
|
||||
author_list = []
|
||||
autor_roles = []
|
||||
for author in book.authors:
|
||||
autor_roles.append({"Name":author.name, "Role":"Author"})
|
||||
author_list.append(author.name)
|
||||
return {"ContributorRoles": autor_roles, "Contributors":author_list}
|
||||
return {"ContributorRoles": [{"Name":book.authors[0].name, "Role":"Author"}], "Contributors": book.authors[0].name}
|
||||
author_list = []
|
||||
autor_roles = []
|
||||
for author in book.authors:
|
||||
autor_roles.append({"Name":author.name}) #.encode('unicode-escape').decode('latin-1')
|
||||
author_list.append(author.name)
|
||||
return {"ContributorRoles": autor_roles, "Contributors":author_list}
|
||||
|
||||
|
||||
def get_publisher(book):
|
||||
@@ -475,9 +484,7 @@ def get_metadata(book):
|
||||
"IsSocialEnabled": True,
|
||||
"Language": "en",
|
||||
"PhoneticPronunciations": {},
|
||||
# TODO: Fix book.pubdate to return a datetime object so that we can easily
|
||||
# convert it to the format Kobo devices expect.
|
||||
"PublicationDate": book.pubdate,
|
||||
"PublicationDate": convert_to_kobo_timestamp_string(book.pubdate),
|
||||
"Publisher": {"Imprint": "", "Name": get_publisher(book),},
|
||||
"RevisionId": book_uuid,
|
||||
"Title": book.title,
|
||||
@@ -492,7 +499,7 @@ def get_metadata(book):
|
||||
"Number": get_seriesindex(book), # ToDo Check int() ?
|
||||
"NumberFloat": float(get_seriesindex(book)),
|
||||
# Get a deterministic id based on the series name.
|
||||
"Id": uuid.uuid3(uuid.NAMESPACE_DNS, name),
|
||||
"Id": str(uuid.uuid3(uuid.NAMESPACE_DNS, name)),
|
||||
}
|
||||
|
||||
return metadata
|
||||
@@ -667,11 +674,8 @@ def HandleTagRemoveItem(tag_id):
|
||||
# Note: Public shelves that aren't owned by the user aren't supported.
|
||||
def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
|
||||
new_tags_last_modified = sync_token.tags_last_modified
|
||||
|
||||
for shelf in ub.session.query(ub.ShelfArchive).filter(
|
||||
func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified,
|
||||
ub.ShelfArchive.user_id == current_user.id
|
||||
):
|
||||
# transmit all archived shelfs independent of last sync (why should this matter?)
|
||||
for shelf in ub.session.query(ub.ShelfArchive).filter(ub.ShelfArchive.user_id == current_user.id):
|
||||
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
|
||||
sync_results.append({
|
||||
"DeletedTag": {
|
||||
@@ -684,7 +688,6 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
|
||||
ub.session.delete(shelf)
|
||||
ub.session_commit()
|
||||
|
||||
|
||||
extra_filters = []
|
||||
if only_kobo_shelves:
|
||||
for shelf in ub.session.query(ub.Shelf).filter(
|
||||
@@ -847,7 +850,7 @@ def get_ub_read_status(kobo_read_status):
|
||||
|
||||
def get_or_create_reading_state(book_id):
|
||||
book_read = ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == book_id,
|
||||
ub.ReadBook.user_id == current_user.id).one_or_none()
|
||||
ub.ReadBook.user_id == int(current_user.id)).one_or_none()
|
||||
if not book_read:
|
||||
book_read = ub.ReadBook(user_id=current_user.id, book_id=book_id)
|
||||
if not book_read.kobo_reading_state:
|
||||
@@ -961,6 +964,8 @@ def HandleBookDeletionRequest(book_uuid):
|
||||
|
||||
ub.session.merge(archived_book)
|
||||
ub.session_commit()
|
||||
if archived_book.is_archived:
|
||||
kobo_sync_status.remove_synced_book(book_id)
|
||||
return "", 204
|
||||
|
||||
|
||||
@@ -984,16 +989,41 @@ def HandleUserRequest(dummy=None):
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/user/loyalty/benefits", methods=["GET"])
|
||||
def handle_benefits():
|
||||
if config.config_kobo_proxy:
|
||||
return redirect_or_proxy_request()
|
||||
else:
|
||||
return make_response(jsonify({"Benefits": {}}))
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/analytics/gettests", methods=["GET", "POST"])
|
||||
def handle_getests():
|
||||
if config.config_kobo_proxy:
|
||||
return redirect_or_proxy_request()
|
||||
else:
|
||||
testkey = request.headers.get("X-Kobo-userkey","")
|
||||
return make_response(jsonify({"Result": "Success", "TestKey":testkey, "Tests": {}}))
|
||||
|
||||
|
||||
@csrf.exempt
|
||||
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/<dummy>/reviews", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/featured/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/featured/", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/external/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/series/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/<dummy>", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/books/<dummy>/", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products/deals", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/products", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
|
||||
@kobo.route("/v1/deals", methods=["GET", "POST"])
|
||||
def HandleProductsRequest(dummy=None):
|
||||
log.debug("Unimplemented Products Request received: %s", request.base_url)
|
||||
return redirect_or_proxy_request()
|
||||
|
||||
+30
-40
@@ -62,6 +62,7 @@ particular calls to non-Kobo specific endpoints such as the CalibreWeb book down
|
||||
from binascii import hexlify
|
||||
from datetime import datetime
|
||||
from os import urandom
|
||||
from functools import wraps
|
||||
|
||||
from flask import g, Blueprint, url_for, abort, request
|
||||
from flask_login import login_user, current_user, login_required
|
||||
@@ -70,11 +71,6 @@ from flask_babel import gettext as _
|
||||
from . import logger, config, calibre_db, db, helper, ub, lm
|
||||
from .render_template import render_title_template
|
||||
|
||||
try:
|
||||
from functools import wraps
|
||||
except ImportError:
|
||||
pass # We're not using Python 3
|
||||
|
||||
|
||||
log = logger.create()
|
||||
|
||||
@@ -122,55 +118,49 @@ kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
|
||||
@kobo_auth.route("/generate_auth_token/<int:user_id>")
|
||||
@login_required
|
||||
def generate_auth_token(user_id):
|
||||
warning = False
|
||||
host_list = request.host.rsplit(':')
|
||||
if len(host_list) == 1:
|
||||
host = ':'.join(host_list)
|
||||
else:
|
||||
host = ':'.join(host_list[0:-1])
|
||||
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'):
|
||||
warning = _('PLease access calibre-web from non localhost to get valid api_endpoint for kobo device')
|
||||
return render_title_template(
|
||||
"generate_kobo_auth_url.html",
|
||||
title=_(u"Kobo Setup"),
|
||||
warning = warning
|
||||
)
|
||||
else:
|
||||
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
|
||||
ub.RemoteAuthToken.user_id == user_id
|
||||
).filter(ub.RemoteAuthToken.token_type==1).first()
|
||||
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f') or host == "[::1]":
|
||||
warning = _('Please access Calibre-Web from non localhost to get valid api_endpoint for kobo device')
|
||||
|
||||
if not auth_token:
|
||||
auth_token = ub.RemoteAuthToken()
|
||||
auth_token.user_id = user_id
|
||||
auth_token.expiration = datetime.max
|
||||
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
|
||||
auth_token.token_type = 1
|
||||
# Generate auth token if none is existing for this user
|
||||
auth_token = ub.session.query(ub.RemoteAuthToken).filter(
|
||||
ub.RemoteAuthToken.user_id == user_id
|
||||
).filter(ub.RemoteAuthToken.token_type==1).first()
|
||||
|
||||
ub.session.add(auth_token)
|
||||
ub.session_commit()
|
||||
if not auth_token:
|
||||
auth_token = ub.RemoteAuthToken()
|
||||
auth_token.user_id = user_id
|
||||
auth_token.expiration = datetime.max
|
||||
auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8")
|
||||
auth_token.token_type = 1
|
||||
|
||||
books = calibre_db.session.query(db.Books).join(db.Data).all()
|
||||
ub.session.add(auth_token)
|
||||
ub.session_commit()
|
||||
|
||||
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)
|
||||
books = calibre_db.session.query(db.Books).join(db.Data).all()
|
||||
|
||||
return render_title_template(
|
||||
"generate_kobo_auth_url.html",
|
||||
title=_(u"Kobo Setup"),
|
||||
kobo_auth_url=url_for(
|
||||
"kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True
|
||||
),
|
||||
warning = False
|
||||
)
|
||||
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(
|
||||
"generate_kobo_auth_url.html",
|
||||
title=_(u"Kobo Setup"),
|
||||
auth_token=auth_token.auth_token,
|
||||
warning = warning
|
||||
)
|
||||
|
||||
|
||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>")
|
||||
@kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_auth_token(user_id):
|
||||
# Invalidate any prevously generated Kobo Auth token for this user.
|
||||
# Invalidate any previously generated Kobo Auth token for this user
|
||||
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
|
||||
.filter(ub.RemoteAuthToken.token_type==1).delete()
|
||||
|
||||
|
||||
+53
-36
@@ -20,52 +20,69 @@
|
||||
from flask_login import current_user
|
||||
from . import ub
|
||||
import datetime
|
||||
from sqlalchemy.sql.expression import or_
|
||||
|
||||
from sqlalchemy.sql.expression import or_, and_, true
|
||||
from sqlalchemy import exc
|
||||
|
||||
# Add the current book id to kobo_synced_books table for current user, if entry is already present,
|
||||
# do nothing (safety precaution)
|
||||
def add_synced_books(book_id):
|
||||
synced_book = ub.KoboSyncedBooks()
|
||||
synced_book.user_id = current_user.id
|
||||
synced_book.book_id = book_id
|
||||
ub.session.add(synced_book)
|
||||
ub.session_commit()
|
||||
is_present = ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id)\
|
||||
.filter(ub.KoboSyncedBooks.user_id == current_user.id).count()
|
||||
if not is_present:
|
||||
synced_book = ub.KoboSyncedBooks()
|
||||
synced_book.user_id = current_user.id
|
||||
synced_book.book_id = book_id
|
||||
ub.session.add(synced_book)
|
||||
ub.session_commit()
|
||||
|
||||
|
||||
def remove_synced_book(book_id):
|
||||
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).delete()
|
||||
ub.session_commit()
|
||||
# Select all entries of current book in kobo_synced_books table, which are from current user and delete them
|
||||
def remove_synced_book(book_id, all=False, session=None):
|
||||
if not all:
|
||||
user = ub.KoboSyncedBooks.user_id == current_user.id
|
||||
else:
|
||||
user = true()
|
||||
if not session:
|
||||
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete()
|
||||
ub.session_commit()
|
||||
else:
|
||||
session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).filter(user).delete()
|
||||
ub.session_commit(sess=session)
|
||||
|
||||
def add_archived_books(book_id):
|
||||
archived_book = (
|
||||
ub.session.query(ub.ArchivedBook)
|
||||
.filter(ub.ArchivedBook.book_id == book_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def change_archived_books(book_id, state=None, message=None):
|
||||
archived_book = ub.session.query(ub.ArchivedBook).filter(and_(ub.ArchivedBook.user_id == int(current_user.id),
|
||||
ub.ArchivedBook.book_id == book_id)).first()
|
||||
if not archived_book:
|
||||
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
|
||||
archived_book.is_archived = True
|
||||
|
||||
archived_book.is_archived = state if state else not archived_book.is_archived
|
||||
archived_book.last_modified = datetime.datetime.utcnow()
|
||||
|
||||
ub.session.merge(archived_book)
|
||||
ub.session_commit()
|
||||
ub.session_commit(message)
|
||||
return archived_book.is_archived
|
||||
|
||||
|
||||
# select all books which are synced by the current user and do not belong to a synced shelf and set them to archive
|
||||
# select all shelves from current user which are synced and do not belong to the "only sync" shelves
|
||||
def update_on_sync_shelfs(user_id):
|
||||
books_to_archive = (ub.session.query(ub.KoboSyncedBooks)
|
||||
.join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True)
|
||||
.join(ub.Shelf, ub.Shelf.user_id == user_id, isouter=True)
|
||||
.filter(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync == None))
|
||||
.filter(ub.KoboSyncedBooks.user_id == user_id).all())
|
||||
for b in books_to_archive:
|
||||
change_archived_books(b.book_id, True)
|
||||
ub.session.query(ub.KoboSyncedBooks) \
|
||||
.filter(ub.KoboSyncedBooks.book_id == b.book_id) \
|
||||
.filter(ub.KoboSyncedBooks.user_id == user_id).delete()
|
||||
ub.session_commit()
|
||||
|
||||
# select all books which are synced by the current user and do not belong to a synced shelf and them to archive
|
||||
# select all shelfs from current user which are synced and do not belong to the "only sync" shelfs
|
||||
def update_on_sync_shelfs(content_id):
|
||||
books_to_archive = (ub.session.query(ub.KoboSyncedBooks)
|
||||
.join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True)
|
||||
.join(ub.Shelf, ub.Shelf.user_id == content_id, isouter=True)
|
||||
.filter(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync == None))
|
||||
.filter(ub.KoboSyncedBooks.user_id == content_id).all())
|
||||
for b in books_to_archive:
|
||||
add_archived_books(b.book_id)
|
||||
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == b.book_id).filter(ub.KoboSyncedBooks.user_id == content_id).delete()
|
||||
ub.session_commit()
|
||||
|
||||
shelfs_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == content_id).filter(
|
||||
ub.Shelf.kobo_sync == 0).all()
|
||||
for a in shelfs_to_archive:
|
||||
ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=content_id))
|
||||
ub.session_commit()
|
||||
# Search all shelf which are currently not synced
|
||||
shelves_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == user_id).filter(
|
||||
ub.Shelf.kobo_sync == 0).all()
|
||||
for a in shelves_to_archive:
|
||||
ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=user_id))
|
||||
ub.session_commit()
|
||||
|
||||
+4
-9
@@ -42,20 +42,15 @@ logging.addLevelName(logging.CRITICAL, "CRIT")
|
||||
|
||||
class _Logger(logging.Logger):
|
||||
|
||||
def debug_or_exception(self, message, *args, **kwargs):
|
||||
def debug_or_exception(self, message, stacklevel=2, *args, **kwargs):
|
||||
if sys.version_info > (3, 7):
|
||||
if is_debug_enabled():
|
||||
self.exception(message, stacklevel=2, *args, **kwargs)
|
||||
self.exception(message, stacklevel=stacklevel, *args, **kwargs)
|
||||
else:
|
||||
self.error(message, stacklevel=2, *args, **kwargs)
|
||||
elif sys.version_info > (3, 0):
|
||||
if is_debug_enabled():
|
||||
self.exception(message, stack_info=True, *args, **kwargs)
|
||||
else:
|
||||
self.error(message, *args, **kwargs)
|
||||
self.error(message, stacklevel=stacklevel, *args, **kwargs)
|
||||
else:
|
||||
if is_debug_enabled():
|
||||
self.exception(message, *args, **kwargs)
|
||||
self.exception(message, stack_info=True, *args, **kwargs)
|
||||
else:
|
||||
self.error(message, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class ComicVine(Metadata):
|
||||
__name__ = "ComicVine"
|
||||
__id__ = "comicvine"
|
||||
|
||||
def search(self, query, __):
|
||||
def search(self, query, generic_cover=""):
|
||||
val = list()
|
||||
apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6"
|
||||
if self.active:
|
||||
@@ -36,7 +36,7 @@ class ComicVine(Metadata):
|
||||
|
||||
result = requests.get("https://comicvine.gamespot.com/api/search?api_key="
|
||||
+ apikey + "&resources=issue&query=" + query + "&sort=name:desc&format=json", headers=headers)
|
||||
for r in result.json()['results']:
|
||||
for r in result.json().get('results'):
|
||||
seriesTitle = r['volume'].get('name', "")
|
||||
if r.get('store_date'):
|
||||
dateFomers = r.get('store_date')
|
||||
|
||||
@@ -26,14 +26,14 @@ class Google(Metadata):
|
||||
__name__ = "Google"
|
||||
__id__ = "google"
|
||||
|
||||
def search(self, query, __):
|
||||
def search(self, query, generic_cover=""):
|
||||
if self.active:
|
||||
val = list()
|
||||
result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+"))
|
||||
for r in result.json()['items']:
|
||||
for r in result.json().get('items'):
|
||||
v = dict()
|
||||
v['id'] = r['id']
|
||||
v['title'] = r['volumeInfo']['title']
|
||||
v['title'] = r['volumeInfo'].get('title',"")
|
||||
v['authors'] = r['volumeInfo'].get('authors', [])
|
||||
v['description'] = r['volumeInfo'].get('description', "")
|
||||
v['publisher'] = r['volumeInfo'].get('publisher', "")
|
||||
|
||||
@@ -20,7 +20,6 @@ from scholarly import scholarly
|
||||
|
||||
from cps.services.Metadata import Metadata
|
||||
|
||||
|
||||
class scholar(Metadata):
|
||||
__name__ = "Google Scholar"
|
||||
__id__ = "googlescholar"
|
||||
@@ -32,7 +31,7 @@ class scholar(Metadata):
|
||||
i = 0
|
||||
for publication in scholar_gen:
|
||||
v = dict()
|
||||
v['id'] = "1234" # publication['bib'].get('title')
|
||||
v['id'] = publication['url_scholarbib'].split(':')[1]
|
||||
v['title'] = publication['bib'].get('title')
|
||||
v['authors'] = publication['bib'].get('author', [])
|
||||
v['description'] = publication['bib'].get('abstract', "")
|
||||
@@ -41,10 +40,10 @@ class scholar(Metadata):
|
||||
v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01"
|
||||
else:
|
||||
v['publishedDate'] = ""
|
||||
v['tags'] = ""
|
||||
v['ratings'] = 0
|
||||
v['tags'] = []
|
||||
v['rating'] = 0
|
||||
v['series'] = ""
|
||||
v['cover'] = generic_cover
|
||||
v['cover'] = ""
|
||||
v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "",
|
||||
v['source'] = {
|
||||
"id": self.__id__,
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import datetime
|
||||
from functools import wraps
|
||||
|
||||
@@ -35,8 +34,6 @@ from .pagination import Pagination
|
||||
from .web import render_read_books
|
||||
from .usermanagement import load_user_from_request
|
||||
from flask_babel import gettext as _
|
||||
from babel import Locale as LC
|
||||
from babel.core import UnknownLocaleError
|
||||
|
||||
opds = Blueprint('opds', __name__)
|
||||
|
||||
|
||||
+2
-4
@@ -27,10 +27,8 @@
|
||||
|
||||
# http://flask.pocoo.org/snippets/62/
|
||||
|
||||
try:
|
||||
from urllib.parse import urlparse, urljoin
|
||||
except ImportError:
|
||||
from urlparse import urlparse, urljoin
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
|
||||
from flask import request, url_for, redirect
|
||||
|
||||
|
||||
@@ -103,9 +103,9 @@ def metadata_search():
|
||||
data = list()
|
||||
active = current_user.view_settings.get('metadata', {})
|
||||
if query:
|
||||
static_cover = url_for('static', filename='generic_cover.jpg')
|
||||
generic_cover = ""
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||
meta = {executor.submit(c.search, query, static_cover): c for c in cl if active.get(c.__id__, True)}
|
||||
meta = {executor.submit(c.search, query, generic_cover): c for c in cl if active.get(c.__id__, True)}
|
||||
for future in concurrent.futures.as_completed(meta):
|
||||
data.extend(future.result())
|
||||
return Response(json.dumps(data), mimetype='application/json')
|
||||
|
||||
@@ -21,11 +21,8 @@ import sys
|
||||
from base64 import b64decode, b64encode
|
||||
from jsonschema import validate, exceptions, __version__
|
||||
from datetime import datetime
|
||||
try:
|
||||
# pylint: disable=unused-import
|
||||
from urllib import unquote
|
||||
except ImportError:
|
||||
from urllib.parse import unquote
|
||||
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import json
|
||||
from .. import logger
|
||||
@@ -182,10 +179,9 @@ class SyncToken:
|
||||
return b64encode_json(token)
|
||||
|
||||
def __str__(self):
|
||||
return "{},{},{},{},{},{}".format(self.raw_kobo_store_token,
|
||||
self.books_last_created,
|
||||
return "{},{},{},{},{},{}".format(self.books_last_created,
|
||||
self.books_last_modified,
|
||||
self.archive_last_modified,
|
||||
self.reading_state_last_modified,
|
||||
self.tags_last_modified)
|
||||
self.tags_last_modified, self.raw_kobo_store_token)
|
||||
#self.books_last_id)
|
||||
|
||||
@@ -85,7 +85,7 @@ class WorkerThread(threading.Thread):
|
||||
def add(cls, user, task):
|
||||
ins = cls.getInstance()
|
||||
ins.num += 1
|
||||
log.debug("Add Task for user: {}: {}".format(user, task))
|
||||
log.debug("Add Task for user: {} - {}".format(user, task))
|
||||
ins.queue.put(QueuedTask(
|
||||
num=ins.num,
|
||||
user=user,
|
||||
|
||||
+22
-12
@@ -56,7 +56,7 @@ def check_shelf_view_permissions(cur_shelf):
|
||||
return True
|
||||
|
||||
|
||||
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>")
|
||||
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
||||
@login_required
|
||||
def add_to_shelf(shelf_id, book_id):
|
||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||
@@ -112,7 +112,7 @@ def add_to_shelf(shelf_id, book_id):
|
||||
return "", 204
|
||||
|
||||
|
||||
@shelf.route("/shelf/massadd/<int:shelf_id>")
|
||||
@shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
|
||||
@login_required
|
||||
def search_to_shelf(shelf_id):
|
||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
@@ -122,8 +122,8 @@ def search_to_shelf(shelf_id):
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
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")
|
||||
log.warning("You are not allowed to add a book to the shelf".format(shelf.name))
|
||||
flash(_(u"You are not allowed to add a book to the shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]:
|
||||
@@ -164,7 +164,7 @@ def search_to_shelf(shelf_id):
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
|
||||
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>")
|
||||
@shelf.route("/shelf/remove/<int:shelf_id>/<int:book_id>", methods=["POST"])
|
||||
@login_required
|
||||
def remove_from_shelf(shelf_id, book_id):
|
||||
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||
@@ -215,10 +215,10 @@ def remove_from_shelf(shelf_id, book_id):
|
||||
else:
|
||||
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"),
|
||||
category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403
|
||||
return "Sorry you are not allowed to remove a book from this shelf", 403
|
||||
|
||||
|
||||
@shelf.route("/shelf/create", methods=["GET", "POST"])
|
||||
@@ -228,6 +228,7 @@ def create_shelf():
|
||||
return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate")
|
||||
|
||||
|
||||
|
||||
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_shelf(shelf_id):
|
||||
@@ -244,12 +245,20 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
|
||||
# calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count()
|
||||
if request.method == "POST":
|
||||
to_save = request.form.to_dict()
|
||||
shelf.is_public = 1 if to_save.get("is_public") else 0
|
||||
if not current_user.role_edit_shelfs() and to_save.get("is_public") == "on":
|
||||
flash(_(u"Sorry you are not allowed to create a public shelf"), category="error")
|
||||
return redirect(url_for('web.index'))
|
||||
is_public = 1 if to_save.get("is_public") == "on" else 0
|
||||
if config.config_kobo_sync:
|
||||
shelf.kobo_sync = True if to_save.get("kobo_sync") else False
|
||||
if shelf.kobo_sync:
|
||||
ub.session.query(ub.ShelfArchive).filter(ub.ShelfArchive.user_id == current_user.id).filter(
|
||||
ub.ShelfArchive.uuid == shelf.uuid).delete()
|
||||
ub.session_commit()
|
||||
shelf_title = to_save.get("title", "")
|
||||
if check_shelf_is_unique(shelf, shelf_title, shelf_id):
|
||||
if check_shelf_is_unique(shelf, shelf_title, is_public, shelf_id):
|
||||
shelf.name = shelf_title
|
||||
shelf.is_public = is_public
|
||||
if not shelf_id:
|
||||
shelf.user_id = int(current_user.id)
|
||||
ub.session.add(shelf)
|
||||
@@ -280,12 +289,12 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
|
||||
sync_only_selected_shelves=sync_only_selected_shelves)
|
||||
|
||||
|
||||
def check_shelf_is_unique(shelf, title, shelf_id=False):
|
||||
def check_shelf_is_unique(shelf, title, is_public, shelf_id=False):
|
||||
if shelf_id:
|
||||
ident = ub.Shelf.id != shelf_id
|
||||
else:
|
||||
ident = true()
|
||||
if shelf.is_public == 1:
|
||||
if is_public == 1:
|
||||
is_shelf_name_unique = ub.session.query(ub.Shelf) \
|
||||
.filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \
|
||||
.filter(ident) \
|
||||
@@ -319,12 +328,13 @@ def delete_shelf_helper(cur_shelf):
|
||||
ub.session_commit("successfully deleted Shelf {}".format(cur_shelf.name))
|
||||
|
||||
|
||||
@shelf.route("/shelf/delete/<int:shelf_id>")
|
||||
@shelf.route("/shelf/delete/<int:shelf_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_shelf(shelf_id):
|
||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||
try:
|
||||
delete_shelf_helper(cur_shelf)
|
||||
flash(_("Shelf successfully deleted"), category="success")
|
||||
except InvalidRequestError:
|
||||
ub.session.rollback()
|
||||
log.error("Settings DB is not Writeable")
|
||||
|
||||
@@ -169,9 +169,14 @@ table .bg-dark-danger a { color: #fff; }
|
||||
position: static;
|
||||
}
|
||||
|
||||
.container-fluid .book .cover span .img {
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.container-fluid .book .cover span img {
|
||||
border: 1px solid #fff;
|
||||
bottom: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
@@ -246,8 +251,10 @@ span.glyphicon.glyphicon-tags {
|
||||
}
|
||||
|
||||
.cover .read {
|
||||
left: auto;
|
||||
right: 2px;
|
||||
position: relative;
|
||||
top: -20px;
|
||||
/*left: auto;
|
||||
right: 2px;*/
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
display: inline-block;
|
||||
@@ -422,7 +429,7 @@ div.log {
|
||||
}
|
||||
|
||||
#detailcover { cursor:zoom-in; }
|
||||
#detailcover:-webkit-full-screen { cursor:zoom-out; }
|
||||
#detailcover:-moz-full-screen { cursor:zoom-out; }
|
||||
#detailcover:-ms-fullscreen { cursor:zoom-out; }
|
||||
#detailcover:fullscreen { cursor:zoom-out; }
|
||||
#detailcover:-webkit-full-screen { cursor:zoom-out; border: 0; }
|
||||
#detailcover:-moz-full-screen { cursor:zoom-out; border: 0; }
|
||||
#detailcover:-ms-fullscreen { cursor:zoom-out; border: 0; }
|
||||
#detailcover:fullscreen { cursor:zoom-out; border: 0; }
|
||||
|
||||
@@ -61,8 +61,11 @@ $("#archived_cb").on("change", function() {
|
||||
|
||||
$("#shelf-actions").on("click", "[data-shelf-action]", function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
$.get(this.href)
|
||||
$.ajax({
|
||||
url: this.href,
|
||||
method:"post",
|
||||
data: {csrf_token:$("input[name='csrf_token']").val()},
|
||||
})
|
||||
.done(function() {
|
||||
var $this = $(this);
|
||||
switch ($this.data("shelf-action")) {
|
||||
|
||||
@@ -248,7 +248,7 @@ $("#btn-upload-format").on("change", function () {
|
||||
if (filename.substring(3, 11) === "fakepath") {
|
||||
filename = filename.substring(12);
|
||||
} // Remove c:\fake at beginning from localhost chrome
|
||||
$("#upload-format").html(filename);
|
||||
$("#upload-format").text(filename);
|
||||
});
|
||||
|
||||
$("#btn-upload-cover").on("change", function () {
|
||||
@@ -256,7 +256,7 @@ $("#btn-upload-cover").on("change", function () {
|
||||
if (filename.substring(3, 11) === "fakepath") {
|
||||
filename = filename.substring(12);
|
||||
} // Remove c:\fake at beginning from localhost chrome
|
||||
$("#upload-cover").html(filename);
|
||||
$("#upload-cover").text(filename);
|
||||
});
|
||||
|
||||
$("#xchange").click(function () {
|
||||
|
||||
@@ -21,47 +21,59 @@ var $list = $("#list").isotope({
|
||||
itemSelector: ".book",
|
||||
layoutMode: "fitRows",
|
||||
getSortData: {
|
||||
title: ".title",
|
||||
}
|
||||
title: ".title"
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
$("#desc").click(function() {
|
||||
if (direction === 0) {
|
||||
return;
|
||||
}
|
||||
$("#asc").removeClass("active");
|
||||
$("#desc").addClass("active");
|
||||
|
||||
var page = $(this).data("id");
|
||||
$.ajax({
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../ajax/view",
|
||||
url: getPath() + "/ajax/view",
|
||||
data: "{\"" + page + "\": {\"dir\": \"desc\"}}",
|
||||
});
|
||||
// invert sorting order to make already inverted start order working
|
||||
$list.isotope({
|
||||
sortBy: "name",
|
||||
sortAscending: true
|
||||
sortAscending: !$list.data('isotope').options.sortAscending
|
||||
});
|
||||
direction = 0;
|
||||
});
|
||||
|
||||
$("#asc").click(function() {
|
||||
if (direction === 1) {
|
||||
return;
|
||||
}
|
||||
$("#desc").removeClass("active");
|
||||
$("#asc").addClass("active");
|
||||
|
||||
var page = $(this).data("id");
|
||||
$.ajax({
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../ajax/view",
|
||||
url: getPath() + "/ajax/view",
|
||||
data: "{\"" + page + "\": {\"dir\": \"asc\"}}",
|
||||
});
|
||||
$list.isotope({
|
||||
sortBy: "name",
|
||||
sortAscending: false
|
||||
sortAscending: !$list.data('isotope').options.sortAscending
|
||||
});
|
||||
direction = 1;
|
||||
});
|
||||
|
||||
$("#all").click(function() {
|
||||
$(".char").removeClass("active");
|
||||
$("#all").addClass("active");
|
||||
// go through all elements and make them visible
|
||||
$list.isotope({ filter: function() {
|
||||
return true;
|
||||
@@ -70,6 +82,9 @@ $("#all").click(function() {
|
||||
});
|
||||
|
||||
$(".char").click(function() {
|
||||
$(".char").removeClass("active");
|
||||
$(this).addClass("active");
|
||||
$("#all").removeClass("active");
|
||||
var character = this.innerText;
|
||||
$list.isotope({ filter: function() {
|
||||
return this.attributes["data-id"].value.charAt(0).toUpperCase() === character;
|
||||
|
||||
@@ -19,6 +19,7 @@ var direction = $("#asc").data('order'); // 0=Descending order; 1= ascending or
|
||||
var sort = 0; // Show sorted entries
|
||||
|
||||
$("#sort_name").click(function() {
|
||||
$("#sort_name").toggleClass("active");
|
||||
var className = $("h1").attr("Class") + "_sort_name";
|
||||
var obj = {};
|
||||
obj[className] = sort;
|
||||
@@ -68,12 +69,15 @@ $("#desc").click(function() {
|
||||
if (direction === 0) {
|
||||
return;
|
||||
}
|
||||
$("#asc").removeClass("active");
|
||||
$("#desc").addClass("active");
|
||||
|
||||
var page = $(this).data("id");
|
||||
$.ajax({
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../ajax/view",
|
||||
url: getPath() + "/ajax/view",
|
||||
data: "{\"" + page + "\": {\"dir\": \"desc\"}}",
|
||||
});
|
||||
var index = 0;
|
||||
@@ -112,16 +116,18 @@ $("#desc").click(function() {
|
||||
|
||||
|
||||
$("#asc").click(function() {
|
||||
|
||||
if (direction === 1) {
|
||||
return;
|
||||
}
|
||||
$("#desc").removeClass("active");
|
||||
$("#asc").addClass("active");
|
||||
|
||||
var page = $(this).data("id");
|
||||
$.ajax({
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../ajax/view",
|
||||
url: getPath() + "/ajax/view",
|
||||
data: "{\"" + page + "\": {\"dir\": \"asc\"}}",
|
||||
});
|
||||
var index = 0;
|
||||
@@ -159,6 +165,8 @@ $("#asc").click(function() {
|
||||
});
|
||||
|
||||
$("#all").click(function() {
|
||||
$("#all").addClass("active");
|
||||
$(".char").removeClass("active");
|
||||
var cnt = $("#second").contents();
|
||||
$("#list").append(cnt);
|
||||
// Find count of middle element
|
||||
@@ -176,6 +184,9 @@ $("#all").click(function() {
|
||||
});
|
||||
|
||||
$(".char").click(function() {
|
||||
$(".char").removeClass("active");
|
||||
$(this).addClass("active");
|
||||
$("#all").removeClass("active");
|
||||
var character = this.innerText;
|
||||
var count = 0;
|
||||
var index = 0;
|
||||
|
||||
@@ -28,16 +28,19 @@ $(function () {
|
||||
|
||||
function populateForm (book) {
|
||||
tinymce.get("description").setContent(book.description);
|
||||
var uniqueTags = [];
|
||||
var uniqueTags = $.map($("#tags").val().split(","), $.trim);
|
||||
if ( uniqueTags.length == 1 && uniqueTags[0] == "") {
|
||||
uniqueTags = [];
|
||||
}
|
||||
$.each(book.tags, function(i, el) {
|
||||
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
|
||||
});
|
||||
var ampSeparatedAuthors = (book.authors || []).join(" & ");
|
||||
$("#bookAuthor").val(ampSeparatedAuthors);
|
||||
$("#book_title").val(book.title);
|
||||
$("#tags").val(uniqueTags.join(","));
|
||||
$("#tags").val(uniqueTags.join(", "));
|
||||
$("#rating").data("rating").setValue(Math.round(book.rating));
|
||||
if(book.cover !== null){
|
||||
if(book.cover && $("#cover_url").length){
|
||||
$(".cover img").attr("src", book.cover);
|
||||
$("#cover_url").val(book.cover);
|
||||
}
|
||||
@@ -125,9 +128,7 @@ $(function () {
|
||||
e.preventDefault();
|
||||
keyword = $("#keyword").val();
|
||||
$('.pill').each(function(){
|
||||
// console.log($(this).data('control'));
|
||||
$(this).data("initial", $(this).prop('checked'));
|
||||
// console.log($(this).data('initial'));
|
||||
});
|
||||
doSearch(keyword);
|
||||
});
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
!function(a){a.fn.datepicker.dates.ko={days:["일요일","월요일","화요일","수요일","목요일","금요일","토요일"],daysShort:["일","월","화","수","목","금","토"],daysMin:["일","월","화","수","목","금","토"],months:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],monthsShort:["1월","2월","3월","4월","5월","6월","7월","8월","9월","10월","11월","12월"],today:"오늘",clear:"삭제",format:"yyyy-mm-dd",titleFormat:"yyyy년mm월",weekStart:0}}(jQuery);
|
||||
@@ -0,0 +1,261 @@
|
||||
tinymce.addI18n('ko_KR',{
|
||||
"Redo": "\ub2e4\uc2dc\uc2e4\ud589",
|
||||
"Undo": "\uc2e4\ud589\ucde8\uc18c",
|
||||
"Cut": "\uc798\ub77c\ub0b4\uae30",
|
||||
"Copy": "\ubcf5\uc0ac\ud558\uae30",
|
||||
"Paste": "\ubd99\uc5ec\ub123\uae30",
|
||||
"Select all": "\uc804\uccb4\uc120\ud0dd",
|
||||
"New document": "\uc0c8 \ubb38\uc11c",
|
||||
"Ok": "\ud655\uc778",
|
||||
"Cancel": "\ucde8\uc18c",
|
||||
"Visual aids": "\uc2dc\uac01\uad50\uc7ac",
|
||||
"Bold": "\uad75\uac8c",
|
||||
"Italic": "\uae30\uc6b8\uc784\uaf34",
|
||||
"Underline": "\ubc11\uc904",
|
||||
"Strikethrough": "\ucde8\uc18c\uc120",
|
||||
"Superscript": "\uc717\ucca8\uc790",
|
||||
"Subscript": "\uc544\ub798\ucca8\uc790",
|
||||
"Clear formatting": "\ud3ec\ub9f7\ucd08\uae30\ud654",
|
||||
"Align left": "\uc67c\ucabd\uc815\ub82c",
|
||||
"Align center": "\uac00\uc6b4\ub370\uc815\ub82c",
|
||||
"Align right": "\uc624\ub978\ucabd\uc815\ub82c",
|
||||
"Justify": "\uc591\ucabd\uc815\ub82c",
|
||||
"Bullet list": "\uc810\ub9ac\uc2a4\ud2b8",
|
||||
"Numbered list": "\uc22b\uc790\ub9ac\uc2a4\ud2b8",
|
||||
"Decrease indent": "\ub0b4\uc5b4\uc4f0\uae30",
|
||||
"Increase indent": "\ub4e4\uc5ec\uc4f0\uae30",
|
||||
"Close": "\ub2eb\uae30",
|
||||
"Formats": "\ud3ec\ub9f7",
|
||||
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\ube0c\ub77c\uc6b0\uc838\uac00 \ud074\ub9bd\ubcf4\ub4dc \uc811\uadfc\uc744 \ud5c8\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. Ctrl+X\/C\/V \ud0a4\ub97c \uc774\uc6a9\ud574 \uc8fc\uc138\uc694.",
|
||||
"Headers": "\uc2a4\ud0c0\uc77c",
|
||||
"Header 1": "\uc81c\ubaa9 1",
|
||||
"Header 2": "\uc81c\ubaa9 2",
|
||||
"Header 3": "\uc81c\ubaa9 3",
|
||||
"Header 4": "\uc81c\ubaa9 4",
|
||||
"Header 5": "\uc81c\ubaa9 5",
|
||||
"Header 6": "\uc81c\ubaa9 6",
|
||||
"Headings": "\uc81c\ubaa9",
|
||||
"Heading 1": "\uc81c\ubaa9 1",
|
||||
"Heading 2": "\uc81c\ubaa9 2",
|
||||
"Heading 3": "\uc81c\ubaa9 3",
|
||||
"Heading 4": "\uc81c\ubaa9 4",
|
||||
"Heading 5": "\uc81c\ubaa9 5",
|
||||
"Heading 6": "\uc81c\ubaa9 6",
|
||||
"Preformatted": "Preformatted",
|
||||
"Div": "\uad6c\ubd84",
|
||||
"Pre": "Pre",
|
||||
"Code": "\ucf54\ub4dc",
|
||||
"Paragraph": "\ub2e8\ub77d",
|
||||
"Blockquote": "\uad6c\ud68d",
|
||||
"Inline": "\ub77c\uc778 \uc124\uc815",
|
||||
"Blocks": "\ube14\ub85d \uc124\uc815",
|
||||
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\uc2a4\ud0c0\uc77c\ubcf5\uc0ac \ub044\uae30. \uc774 \uc635\uc158\uc744 \ub044\uae30 \uc804\uc5d0\ub294 \ubcf5\uc0ac \uc2dc, \uc2a4\ud0c0\uc77c\uc774 \ubcf5\uc0ac\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.",
|
||||
"Font Family": "\uae00\uaf34",
|
||||
"Font Sizes": "\ud3f0\ud2b8 \uc0ac\uc774\uc988",
|
||||
"Class": "\ud074\ub798\uc2a4",
|
||||
"Browse for an image": "\uc774\ubbf8\uc9c0 \ucc3e\uae30",
|
||||
"OR": "\ud639\uc740",
|
||||
"Drop an image here": "\uc774\ubbf8\uc9c0 \ub4dc\ub86d",
|
||||
"Upload": "\uc5c5\ub85c\ub4dc",
|
||||
"Block": "\ube14\ub85d",
|
||||
"Align": "\uc815\ub82c",
|
||||
"Default": "\uae30\ubcf8",
|
||||
"Circle": "\uc6d0",
|
||||
"Disc": "\uc6d0\ubc18",
|
||||
"Square": "\uc0ac\uac01",
|
||||
"Lower Alpha": "\uc54c\ud30c\ubcb3 \uc18c\ubb38\uc790",
|
||||
"Lower Greek": "\uadf8\ub9ac\uc2a4\uc5b4 \uc18c\ubb38\uc790",
|
||||
"Lower Roman": "\ub85c\ub9c8\uc790 \uc18c\ubb38\uc790",
|
||||
"Upper Alpha": "\uc54c\ud30c\ubcb3 \uc18c\ubb38\uc790",
|
||||
"Upper Roman": "\ub85c\ub9c8\uc790 \ub300\ubb38\uc790",
|
||||
"Anchor": "\uc575\ucee4",
|
||||
"Name": "\uc774\ub984",
|
||||
"Id": "\uc544\uc774\ub514",
|
||||
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\uc544\uc774\ub514\ub294 \ubb38\uc790, \uc22b\uc790, \ub300\uc2dc, \uc810, \ucf5c\ub860 \ub610\ub294 \ubc11\uc904\ub85c \uc2dc\uc791\ud574\uc57c\ud569\ub2c8\ub2e4.",
|
||||
"You have unsaved changes are you sure you want to navigate away?": "\uc800\uc7a5\ud558\uc9c0 \uc54a\uc740 \uc815\ubcf4\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uc774 \ud398\uc774\uc9c0\ub97c \ubc97\uc5b4\ub098\uc2dc\uaca0\uc2b5\ub2c8\uae4c?",
|
||||
"Restore last draft": "\ub9c8\uc9c0\ub9c9 \ucd08\uc548 \ubcf5\uc6d0",
|
||||
"Special character": "\ud2b9\uc218\ubb38\uc790",
|
||||
"Source code": "\uc18c\uc2a4\ucf54\ub4dc",
|
||||
"Insert\/Edit code sample": "\ucf54\ub4dc\uc0d8\ud50c \uc0bd\uc785\/\ud3b8\uc9d1",
|
||||
"Language": "\uc5b8\uc5b4",
|
||||
"Code sample": "\ucf54\ub4dc\uc0d8\ud50c",
|
||||
"Color": "\uc0c9\uc0c1",
|
||||
"R": "R",
|
||||
"G": "G",
|
||||
"B": "B",
|
||||
"Left to right": "\uc67c\ucabd\uc5d0\uc11c \uc624\ub978\ucabd",
|
||||
"Right to left": "\uc624\ub978\ucabd\uc5d0\uc11c \uc67c\ucabd",
|
||||
"Emoticons": "\uc774\ubaa8\ud2f0\ucf58",
|
||||
"Document properties": "\ubb38\uc11c \uc18d\uc131",
|
||||
"Title": "\uc81c\ubaa9",
|
||||
"Keywords": "\ud0a4\uc6cc\ub4dc",
|
||||
"Description": "\uc124\uba85",
|
||||
"Robots": "\ub85c\ubd07",
|
||||
"Author": "\uc800\uc790",
|
||||
"Encoding": "\uc778\ucf54\ub529",
|
||||
"Fullscreen": "\uc804\uccb4\ud654\uba74",
|
||||
"Action": "\ub3d9\uc791",
|
||||
"Shortcut": "\ub2e8\ucd95\ud0a4",
|
||||
"Help": "\ub3c4\uc6c0\ub9d0",
|
||||
"Address": "\uc8fc\uc18c",
|
||||
"Focus to menubar": "\uba54\ub274\uc5d0 \ud3ec\ucee4\uc2a4",
|
||||
"Focus to toolbar": "\ud234\ubc14\uc5d0 \ud3ec\ucee4\uc2a4",
|
||||
"Focus to element path": "element path\uc5d0 \ud3ec\ucee4\uc2a4",
|
||||
"Focus to contextual toolbar": "\ucf04\ud14d\uc2a4\ud2b8 \ud234\ubc14\uc5d0 \ud3ec\ucee4\uc2a4",
|
||||
"Insert link (if link plugin activated)": "\ub9c1\ud06c \uc0bd\uc785 (link \ud50c\ub7ec\uadf8\uc778\uc774 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c)",
|
||||
"Save (if save plugin activated)": "\uc800\uc7a5 (save \ud50c\ub7ec\uadf8\uc778\uc774 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c)",
|
||||
"Find (if searchreplace plugin activated)": "\ucc3e\uae30(searchreplace \ud50c\ub7ec\uadf8\uc778\uc774 \ud65c\uc131\ud654\ub41c \uc0c1\ud0dc\uc5d0\uc11c)",
|
||||
"Plugins installed ({0}):": "\uc124\uce58\ub41c \ud50c\ub7ec\uadf8\uc778 ({0}):",
|
||||
"Premium plugins:": "\uace0\uae09 \ud50c\ub7ec\uadf8\uc778",
|
||||
"Learn more...": "\uc880 \ub354 \uc0b4\ud3b4\ubcf4\uae30",
|
||||
"You are using {0}": "{0}\ub97c \uc0ac\uc6a9\uc911",
|
||||
"Plugins": "\ud50c\ub7ec\uadf8\uc778",
|
||||
"Handy Shortcuts": "\ub2e8\ucd95\ud0a4",
|
||||
"Horizontal line": "\uac00\ub85c",
|
||||
"Insert\/edit image": "\uc774\ubbf8\uc9c0 \uc0bd\uc785\/\uc218\uc815",
|
||||
"Image description": "\uc774\ubbf8\uc9c0 \uc124\uba85",
|
||||
"Source": "\uc18c\uc2a4",
|
||||
"Dimensions": "\ud06c\uae30",
|
||||
"Constrain proportions": "\uc791\uc5c5 \uc81c\ud55c",
|
||||
"General": "\uc77c\ubc18",
|
||||
"Advanced": "\uace0\uae09",
|
||||
"Style": "\uc2a4\ud0c0\uc77c",
|
||||
"Vertical space": "\uc218\uc9c1 \uacf5\ubc31",
|
||||
"Horizontal space": "\uc218\ud3c9 \uacf5\ubc31",
|
||||
"Border": "\ud14c\ub450\ub9ac",
|
||||
"Insert image": "\uc774\ubbf8\uc9c0 \uc0bd\uc785",
|
||||
"Image": "\uc774\ubbf8\uc9c0",
|
||||
"Image list": "\uc774\ubbf8\uc9c0 \ubaa9\ub85d",
|
||||
"Rotate counterclockwise": "\uc2dc\uacc4\ubc18\ub300\ubc29\ud5a5\uc73c\ub85c \ud68c\uc804",
|
||||
"Rotate clockwise": "\uc2dc\uacc4\ubc29\ud5a5\uc73c\ub85c \ud68c\uc804",
|
||||
"Flip vertically": "\uc218\uc9c1 \ub4a4\uc9d1\uae30",
|
||||
"Flip horizontally": "\uc218\ud3c9 \ub4a4\uc9d1\uae30",
|
||||
"Edit image": "\uc774\ubbf8\uc9c0 \ud3b8\uc9d1",
|
||||
"Image options": "\uc774\ubbf8\uc9c0 \uc635\uc158",
|
||||
"Zoom in": "\ud655\ub300",
|
||||
"Zoom out": "\ucd95\uc18c",
|
||||
"Crop": "\uc790\ub974\uae30",
|
||||
"Resize": "\ud06c\uae30 \uc870\uc808",
|
||||
"Orientation": "\ubc29\ud5a5",
|
||||
"Brightness": "\ubc1d\uae30",
|
||||
"Sharpen": "\uc120\uba85\ud558\uac8c",
|
||||
"Contrast": "\ub300\ube44",
|
||||
"Color levels": "\uc0c9\uc0c1\ub808\ubca8",
|
||||
"Gamma": "\uac10\ub9c8",
|
||||
"Invert": "\ubc18\uc804",
|
||||
"Apply": "\uc801\uc6a9",
|
||||
"Back": "\ub4a4\ub85c",
|
||||
"Insert date\/time": "\ub0a0\uc9dc\/\uc2dc\uac04\uc0bd\uc785",
|
||||
"Date\/time": "\ub0a0\uc9dc\/\uc2dc\uac04",
|
||||
"Insert link": "\ub9c1\ud06c \uc0bd\uc785 ",
|
||||
"Insert\/edit link": "\ub9c1\ud06c \uc0bd\uc785\/\uc218\uc815",
|
||||
"Text to display": "\ubcf8\ubb38",
|
||||
"Url": "\uc8fc\uc18c",
|
||||
"Target": "\ub300\uc0c1",
|
||||
"None": "\uc5c6\uc74c",
|
||||
"New window": "\uc0c8\ucc3d",
|
||||
"Remove link": "\ub9c1\ud06c\uc0ad\uc81c",
|
||||
"Anchors": "\ucc45\uac08\ud53c",
|
||||
"Link": "\ub9c1\ud06c",
|
||||
"Paste or type a link": "\ub9c1\ud06c\ub97c \ubd99\uc5ec\ub123\uac70\ub098 \uc785\ub825\ud558\uc138\uc694",
|
||||
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\ud604\uc7ac E-mail\uc8fc\uc18c\ub97c \uc785\ub825\ud558\uc168\uc2b5\ub2c8\ub2e4. E-mail \uc8fc\uc18c\uc5d0 \ub9c1\ud06c\ub97c \uac78\uae4c\uc694?",
|
||||
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\ud604\uc7ac \uc6f9\uc0ac\uc774\ud2b8 \uc8fc\uc18c\ub97c \uc785\ub825\ud558\uc168\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \uc8fc\uc18c\uc5d0 \ub9c1\ud06c\ub97c \uac78\uae4c\uc694?",
|
||||
"Link list": "\ub9c1\ud06c \ub9ac\uc2a4\ud2b8",
|
||||
"Insert video": "\ube44\ub514\uc624 \uc0bd\uc785",
|
||||
"Insert\/edit video": "\ube44\ub514\uc624 \uc0bd\uc785\/\uc218\uc815",
|
||||
"Insert\/edit media": "\ubbf8\ub514\uc5b4 \uc0bd\uc785\/\uc218\uc815",
|
||||
"Alternative source": "\ub300\uccb4 \uc18c\uc2a4",
|
||||
"Poster": "\ud3ec\uc2a4\ud130",
|
||||
"Paste your embed code below:": "\uc544\ub798\uc5d0 \ucf54\ub4dc\ub97c \ubd99\uc5ec\ub123\uc73c\uc138\uc694:",
|
||||
"Embed": "\uc0bd\uc785",
|
||||
"Media": "\ubbf8\ub514\uc5b4",
|
||||
"Nonbreaking space": "\ub744\uc5b4\uc4f0\uae30",
|
||||
"Page break": "\ud398\uc774\uc9c0 \uad6c\ubd84\uc790",
|
||||
"Paste as text": "\ud14d\uc2a4\ud2b8\ub85c \ubd99\uc5ec\ub123\uae30",
|
||||
"Preview": "\ubbf8\ub9ac\ubcf4\uae30",
|
||||
"Print": "\ucd9c\ub825",
|
||||
"Save": "\uc800\uc7a5",
|
||||
"Find": "\ucc3e\uae30",
|
||||
"Replace with": "\uad50\uccb4",
|
||||
"Replace": "\uad50\uccb4",
|
||||
"Replace all": "\uc804\uccb4 \uad50\uccb4",
|
||||
"Prev": "\uc774\uc804",
|
||||
"Next": "\ub2e4\uc74c",
|
||||
"Find and replace": "\ucc3e\uc544\uc11c \uad50\uccb4",
|
||||
"Could not find the specified string.": "\ubb38\uc790\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
|
||||
"Match case": "\ub300\uc18c\ubb38\uc790 \uc77c\uce58",
|
||||
"Whole words": "\uc804\uccb4 \ub2e8\uc5b4",
|
||||
"Spellcheck": "\ubb38\ubc95\uccb4\ud06c",
|
||||
"Ignore": "\ubb34\uc2dc",
|
||||
"Ignore all": "\uc804\uccb4\ubb34\uc2dc",
|
||||
"Finish": "\uc644\ub8cc",
|
||||
"Add to Dictionary": "\uc0ac\uc804\uc5d0 \ucd94\uac00",
|
||||
"Insert table": "\ud14c\uc774\ube14 \uc0bd\uc785",
|
||||
"Table properties": "\ud14c\uc774\ube14 \uc18d\uc131",
|
||||
"Delete table": "\ud14c\uc774\ube14 \uc0ad\uc81c",
|
||||
"Cell": "\uc140",
|
||||
"Row": "\uc5f4",
|
||||
"Column": "\ud589",
|
||||
"Cell properties": "\uc140 \uc18d",
|
||||
"Merge cells": "\uc140 \ud569\uce58\uae30",
|
||||
"Split cell": "\uc140 \ub098\ub204\uae30",
|
||||
"Insert row before": "\uc774\uc804\uc5d0 \ud589 \uc0bd\uc785",
|
||||
"Insert row after": "\ub2e4\uc74c\uc5d0 \ud589 \uc0bd\uc785",
|
||||
"Delete row": "\ud589 \uc9c0\uc6b0\uae30",
|
||||
"Row properties": "\ud589 \uc18d\uc131",
|
||||
"Cut row": "\ud589 \uc798\ub77c\ub0b4\uae30",
|
||||
"Copy row": "\ud589 \ubcf5\uc0ac",
|
||||
"Paste row before": "\uc774\uc804\uc5d0 \ud589 \ubd99\uc5ec\ub123\uae30",
|
||||
"Paste row after": "\ub2e4\uc74c\uc5d0 \ud589 \ubd99\uc5ec\ub123\uae30",
|
||||
"Insert column before": "\uc774\uc804\uc5d0 \ud589 \uc0bd\uc785",
|
||||
"Insert column after": "\ub2e4\uc74c\uc5d0 \uc5f4 \uc0bd\uc785",
|
||||
"Delete column": "\uc5f4 \uc9c0\uc6b0\uae30",
|
||||
"Cols": "\uc5f4",
|
||||
"Rows": "\ud589",
|
||||
"Width": "\ub113\uc774",
|
||||
"Height": "\ub192\uc774",
|
||||
"Cell spacing": "\uc140 \uac04\uaca9",
|
||||
"Cell padding": "\uc140 \uc548\ucabd \uc5ec\ubc31",
|
||||
"Caption": "\ucea1\uc158",
|
||||
"Left": "\uc67c\ucabd",
|
||||
"Center": "\uac00\uc6b4\ub370",
|
||||
"Right": "\uc624\ub978\ucabd",
|
||||
"Cell type": "\uc140 \ud0c0\uc785",
|
||||
"Scope": "\ubc94\uc704",
|
||||
"Alignment": "\uc815\ub82c",
|
||||
"H Align": "\uac00\ub85c \uc815\ub82c",
|
||||
"V Align": "\uc138\ub85c \uc815\ub82c",
|
||||
"Top": "\uc0c1\ub2e8",
|
||||
"Middle": "\uc911\uac04",
|
||||
"Bottom": "\ud558\ub2e8",
|
||||
"Header cell": "\ud5e4\ub354 \uc140",
|
||||
"Row group": "\ud589 \uadf8\ub8f9",
|
||||
"Column group": "\uc5f4 \uadf8\ub8f9",
|
||||
"Row type": "\ud589 \ud0c0\uc785",
|
||||
"Header": "\ud5e4\ub354",
|
||||
"Body": "\ubc14\ub514",
|
||||
"Footer": "\ud478\ud130",
|
||||
"Border color": "\ud14c\ub450\ub9ac \uc0c9",
|
||||
"Insert template": "\ud15c\ud50c\ub9bf \uc0bd\uc785",
|
||||
"Templates": "\ud15c\ud50c\ub9bf",
|
||||
"Template": "\ud15c\ud50c\ub9bf",
|
||||
"Text color": "\ubb38\uc790 \uc0c9\uae54",
|
||||
"Background color": "\ubc30\uacbd\uc0c9",
|
||||
"Custom...": "\uc9c1\uc811 \uc0c9\uae54 \uc9c0\uc815\ud558\uae30",
|
||||
"Custom color": "\uc9c1\uc811 \uc9c0\uc815\ud55c \uc0c9\uae54",
|
||||
"No color": "\uc0c9\uc0c1 \uc5c6\uc74c",
|
||||
"Table of Contents": "\ubaa9\ucc28",
|
||||
"Show blocks": "\ube14\ub7ed \ubcf4\uc5ec\uc8fc\uae30",
|
||||
"Show invisible characters": "\uc548\ubcf4\uc774\ub294 \ubb38\uc790 \ubcf4\uc774\uae30",
|
||||
"Words: {0}": "\ub2e8\uc5b4: {0}",
|
||||
"{0} words": "{0} \ub2e8\uc5b4",
|
||||
"File": "\ud30c\uc77c",
|
||||
"Edit": "\uc218\uc815",
|
||||
"Insert": "\uc0bd\uc785",
|
||||
"View": "\ubcf4\uae30",
|
||||
"Format": "\ud3ec\ub9f7",
|
||||
"Table": "\ud14c\uc774\ube14",
|
||||
"Tools": "\ub3c4\uad6c",
|
||||
"Powered by {0}": "Powered by {0}",
|
||||
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\uc11c\uc2dd \uc788\ub294 \ud14d\uc2a4\ud2b8 \ud3b8\uc9d1\uae30 \uc785\ub2c8\ub2e4. ALT-F9\ub97c \ub204\ub974\uba74 \uba54\ub274, ALT-F10\ub97c \ub204\ub974\uba74 \ud234\ubc14, ALT-0\uc744 \ub204\ub974\uba74 \ub3c4\uc6c0\ub9d0\uc744 \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4."
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
tinymce.addI18n('zh_CN',{
|
||||
tinymce.addI18n('zh_Hans_CN',{
|
||||
"Redo": "\u91cd\u505a",
|
||||
"Undo": "\u64a4\u9500",
|
||||
"Cut": "\u526a\u5207",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
tinymce.addI18n('zh_TW',{
|
||||
tinymce.addI18n('zh_Hant_TW',{
|
||||
"Redo": "\u91cd\u505a",
|
||||
"Undo": "\u64a4\u92b7",
|
||||
"Cut": "\u526a\u4e0b",
|
||||
@@ -416,4 +416,4 @@ tinymce.addI18n('zh_TW',{
|
||||
"Spellcheck": "\u62fc\u5b57\u6aa2\u67e5",
|
||||
"Caption": "\u8868\u683c\u6a19\u984c",
|
||||
"Insert template": "\u63d2\u5165\u6a23\u7248"
|
||||
});
|
||||
});
|
||||
|
||||
Regular → Executable
+83
-21
@@ -20,6 +20,20 @@ function getPath() {
|
||||
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path
|
||||
}
|
||||
|
||||
function postButton(event, action){
|
||||
event.preventDefault();
|
||||
var newForm = jQuery('<form>', {
|
||||
"action": action,
|
||||
'target': "_top",
|
||||
'method': "post"
|
||||
}).append(jQuery('<input>', {
|
||||
'name': 'csrf_token',
|
||||
'value': $("input[name=\'csrf_token\']").val(),
|
||||
'type': 'hidden'
|
||||
})).appendTo('body');
|
||||
newForm.submit();
|
||||
}
|
||||
|
||||
function elementSorter(a, b) {
|
||||
a = +a.slice(0, -2);
|
||||
b = +b.slice(0, -2);
|
||||
@@ -71,6 +85,22 @@ $(document).on("change", "select[data-controlall]", function() {
|
||||
}
|
||||
});
|
||||
|
||||
/*$(document).on("click", "#sendbtn", function (event) {
|
||||
postButton(event, $(this).data('action'));
|
||||
});
|
||||
|
||||
$(document).on("click", ".sendbutton", function (event) {
|
||||
// $(".sendbutton").on("click", "body", function(event) {
|
||||
postButton(event, $(this).data('action'));
|
||||
});*/
|
||||
|
||||
$(document).on("click", ".postAction", function (event) {
|
||||
// $(".sendbutton").on("click", "body", function(event) {
|
||||
postButton(event, $(this).data('action'));
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Syntax has to be bind not on, otherwise problems with firefox
|
||||
$(".container-fluid").bind("dragenter dragover", function () {
|
||||
if($("#btn-upload").length && !$('body').hasClass('shelforder')) {
|
||||
@@ -168,18 +198,18 @@ function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
|
||||
$confirm.modal('show');
|
||||
}
|
||||
|
||||
$("#delete_confirm").click(function() {
|
||||
$("#delete_confirm").click(function(event) {
|
||||
//get data-id attribute of the clicked element
|
||||
var deleteId = $(this).data("delete-id");
|
||||
var bookFormat = $(this).data("delete-format");
|
||||
var ajaxResponse = $(this).data("ajax");
|
||||
if (bookFormat) {
|
||||
window.location.href = getPath() + "/delete/" + deleteId + "/" + bookFormat;
|
||||
postButton(event, getPath() + "/delete/" + deleteId + "/" + bookFormat);
|
||||
} else {
|
||||
if (ajaxResponse) {
|
||||
path = getPath() + "/ajax/delete/" + deleteId;
|
||||
$.ajax({
|
||||
method:"get",
|
||||
method:"post",
|
||||
url: path,
|
||||
timeout: 900,
|
||||
success:function(data) {
|
||||
@@ -198,8 +228,7 @@ $("#delete_confirm").click(function() {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
window.location.href = getPath() + "/delete/" + deleteId;
|
||||
|
||||
postButton(event, getPath() + "/delete/" + deleteId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,11 +313,7 @@ $(function() {
|
||||
}
|
||||
|
||||
function fillFileTable(path, type, folder, filt) {
|
||||
if (window.location.pathname.endsWith("/basicconfig")) {
|
||||
var request_path = "/../basicconfig/pathchooser/";
|
||||
} else {
|
||||
var request_path = "/../../ajax/pathchooser/";
|
||||
}
|
||||
var request_path = "/../../ajax/pathchooser/";
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
data: {
|
||||
@@ -380,9 +405,11 @@ $(function() {
|
||||
|
||||
$("#restart").click(function() {
|
||||
$.ajax({
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../shutdown",
|
||||
data: {"parameter":0},
|
||||
url: getPath() + "/shutdown",
|
||||
data: JSON.stringify({"parameter":0}),
|
||||
success: function success() {
|
||||
$("#spinner").show();
|
||||
setTimeout(restartTimer, 3000);
|
||||
@@ -391,9 +418,11 @@ $(function() {
|
||||
});
|
||||
$("#shutdown").click(function() {
|
||||
$.ajax({
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: window.location.pathname + "/../../shutdown",
|
||||
data: {"parameter":1},
|
||||
url: getPath() + "/shutdown",
|
||||
data: JSON.stringify({"parameter":1}),
|
||||
success: function success(data) {
|
||||
return alert(data.text);
|
||||
}
|
||||
@@ -451,9 +480,11 @@ $(function() {
|
||||
$("#DialogContent").html("");
|
||||
$("#spinner2").show();
|
||||
$.ajax({
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: getPath() + "/shutdown",
|
||||
data: {"parameter":2},
|
||||
data: JSON.stringify({"parameter":2}),
|
||||
success: function success(data) {
|
||||
$("#spinner2").hide();
|
||||
$("#DialogContent").html(data.text);
|
||||
@@ -504,6 +535,7 @@ $(function() {
|
||||
|
||||
$("#modal_kobo_token")
|
||||
.on("show.bs.modal", function(e) {
|
||||
$(e.relatedTarget).one('focus', function(e){$(this).blur();});
|
||||
var $modalBody = $(this).find(".modal-body");
|
||||
|
||||
// Prevent static assets from loading multiple times
|
||||
@@ -521,6 +553,7 @@ $(function() {
|
||||
.on("hidden.bs.modal", function() {
|
||||
$(this).find(".modal-body").html("...");
|
||||
$("#config_delete_kobo_token").show();
|
||||
$("#kobo_full_sync").show();
|
||||
});
|
||||
|
||||
$("#config_delete_kobo_token").click(function() {
|
||||
@@ -530,10 +563,11 @@ $(function() {
|
||||
$(this).data('value'),
|
||||
function (value) {
|
||||
$.ajax({
|
||||
method: "get",
|
||||
method: "post",
|
||||
url: getPath() + "/kobo_auth/deleteauthtoken/" + value,
|
||||
});
|
||||
$("#config_delete_kobo_token").hide();
|
||||
$("#kobo_full_sync").hide();
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -567,6 +601,33 @@ $(function() {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$("#kobo_full_sync").click(function() {
|
||||
confirmDialog(
|
||||
"btnfullsync",
|
||||
"GeneralDeleteModal",
|
||||
$(this).data('value'),
|
||||
function(value){
|
||||
path = getPath() + "/ajax/fullsync"
|
||||
$.ajax({
|
||||
method:"post",
|
||||
url: path,
|
||||
timeout: 900,
|
||||
success:function(data) {
|
||||
data.forEach(function(item) {
|
||||
if (!jQuery.isEmptyObject(item)) {
|
||||
$( ".navbar" ).after( '<div class="row-fluid text-center" >' +
|
||||
'<div id="flash_'+item.type+'" class="alert alert-'+item.type+'">'+item.message+'</div>' +
|
||||
'</div>');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
$("#user_submit").click(function() {
|
||||
this.closest("form").submit();
|
||||
});
|
||||
@@ -613,7 +674,7 @@ $(function() {
|
||||
else {
|
||||
$("#InvalidDialog").modal('show');
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
changeDbSettings();
|
||||
}
|
||||
}
|
||||
@@ -654,13 +715,14 @@ $(function() {
|
||||
});
|
||||
});
|
||||
|
||||
$("#delete_shelf").click(function() {
|
||||
$("#delete_shelf").click(function(event) {
|
||||
confirmDialog(
|
||||
$(this).attr('id'),
|
||||
"GeneralDeleteModal",
|
||||
$(this).data('value'),
|
||||
function(value){
|
||||
window.location.href = window.location.pathname + "/../../shelf/delete/" + value
|
||||
postButton(event, $("#delete_shelf").data("action"));
|
||||
// $("#delete_shelf").closest("form").submit()
|
||||
}
|
||||
);
|
||||
|
||||
@@ -709,7 +771,8 @@ $(function() {
|
||||
$("#DialogContent").html("");
|
||||
$("#spinner2").show();
|
||||
$.ajax({
|
||||
method:"get",
|
||||
method:"post",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
dataType: "json",
|
||||
url: getPath() + "/import_ldap_users",
|
||||
success: function success(data) {
|
||||
@@ -743,4 +806,3 @@ $(function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -61,11 +61,14 @@ var reader;
|
||||
this.removeBookmark(bookmark);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
var csrftoken = $("input[name='csrf_token']").val();
|
||||
|
||||
// Save to database
|
||||
$.ajax(calibre.bookmarkUrl, {
|
||||
method: "post",
|
||||
data: { bookmark: location || "" }
|
||||
data: { bookmark: location || "" },
|
||||
headers: { "X-CSRFToken": csrftoken }
|
||||
}).fail(function (xhr, status, error) {
|
||||
alert(error);
|
||||
});
|
||||
|
||||
+18
-18
@@ -47,15 +47,15 @@ $(function() {
|
||||
var rows = rowsAfter;
|
||||
|
||||
if (e.type === "uncheck-all") {
|
||||
rows = rowsBefore;
|
||||
selections = [];
|
||||
} else {
|
||||
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
|
||||
return row.id;
|
||||
});
|
||||
|
||||
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
|
||||
selections = window._[func](selections, ids);
|
||||
}
|
||||
|
||||
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
|
||||
return row.id;
|
||||
});
|
||||
|
||||
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
|
||||
selections = window._[func](selections, ids);
|
||||
if (selections.length >= 2) {
|
||||
$("#merge_books").removeClass("disabled");
|
||||
$("#merge_books").attr("aria-disabled", false);
|
||||
@@ -540,14 +540,14 @@ $(function() {
|
||||
var rows = rowsAfter;
|
||||
|
||||
if (e.type === "uncheck-all") {
|
||||
rows = rowsBefore;
|
||||
selections = [];
|
||||
} else {
|
||||
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
|
||||
return row.id;
|
||||
});
|
||||
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
|
||||
selections = window._[func](selections, ids);
|
||||
}
|
||||
|
||||
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
|
||||
return row.id;
|
||||
});
|
||||
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
|
||||
selections = window._[func](selections, ids);
|
||||
handle_header_buttons();
|
||||
});
|
||||
});
|
||||
@@ -631,14 +631,14 @@ function singleUserFormatter(value, row) {
|
||||
}
|
||||
|
||||
function checkboxFormatter(value, row){
|
||||
if(value & this.column)
|
||||
if (value & this.column)
|
||||
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" checked onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', ' + this.column + ')">';
|
||||
else
|
||||
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', ' + this.column + ')">';
|
||||
}
|
||||
|
||||
function singlecheckboxFormatter(value, row){
|
||||
if(value)
|
||||
if (value)
|
||||
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" checked onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', 0)">';
|
||||
else
|
||||
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', 0)">';
|
||||
@@ -793,7 +793,7 @@ function handleListServerResponse (data) {
|
||||
function checkboxChange(checkbox, userId, field, field_index) {
|
||||
$.ajax({
|
||||
method: "post",
|
||||
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
|
||||
url: getPath() + "/ajax/editlistusers/" + field,
|
||||
data: {"pk": userId, "field_index": field_index, "value": checkbox.checked},
|
||||
error: function(data) {
|
||||
handleListServerResponse([{type:"danger", message:data.responseText}])
|
||||
|
||||
+18
-6
@@ -16,7 +16,6 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
@@ -31,7 +30,8 @@ from cps import db
|
||||
from cps import logger, config
|
||||
from cps.subproc_wrapper import process_open
|
||||
from flask_babel import gettext as _
|
||||
from flask import url_for
|
||||
from cps.kobo_sync_status import remove_synced_book
|
||||
from cps.ub import ini
|
||||
|
||||
from cps.tasks.mail import TaskEmail
|
||||
from cps import gdriveutils
|
||||
@@ -147,10 +147,15 @@ class TaskConvert(CalibreTask):
|
||||
try:
|
||||
local_db.session.merge(new_format)
|
||||
local_db.session.commit()
|
||||
if self.settings['new_book_format'].upper() in ['KEPUB', 'EPUB', 'EPUB3']:
|
||||
ub_session = ini()
|
||||
remove_synced_book(book_id, True, ub_session)
|
||||
ub_session.close()
|
||||
except SQLAlchemyError as e:
|
||||
local_db.session.rollback()
|
||||
log.error("Database error: %s", e)
|
||||
local_db.session.close()
|
||||
self._handleError(error_message)
|
||||
return
|
||||
self.results['path'] = cur_book.path
|
||||
self.title = cur_book.title
|
||||
@@ -216,13 +221,16 @@ class TaskConvert(CalibreTask):
|
||||
quotes.append(quotes_index)
|
||||
quotes_index += 1
|
||||
|
||||
p = process_open(command, quotes)
|
||||
p = process_open(command, quotes, newlines=False)
|
||||
except OSError as e:
|
||||
return 1, _(u"Ebook-converter failed: %(error)s", error=e)
|
||||
|
||||
while p.poll() is None:
|
||||
nextline = p.stdout.readline()
|
||||
log.debug(nextline.strip('\r\n'))
|
||||
if isinstance(nextline, bytes):
|
||||
nextline = nextline.decode('utf-8', errors="ignore").strip('\r\n')
|
||||
if nextline:
|
||||
log.debug(nextline)
|
||||
# parse progress string from calibre-converter
|
||||
progress = re.search(r"(\d+)%\s.*", nextline)
|
||||
if progress:
|
||||
@@ -235,11 +243,15 @@ class TaskConvert(CalibreTask):
|
||||
calibre_traceback = p.stderr.readlines()
|
||||
error_message = ""
|
||||
for ele in calibre_traceback:
|
||||
log.debug(ele.strip('\n'))
|
||||
ele = ele.decode('utf-8', errors="ignore").strip('\n')
|
||||
log.debug(ele)
|
||||
if not ele.startswith('Traceback') and not ele.startswith(' File'):
|
||||
error_message = _("Calibre failed with error: %(error)s", error=ele.strip('\n'))
|
||||
error_message = _("Calibre failed with error: %(error)s", error=ele)
|
||||
return check, error_message
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "Convert"
|
||||
|
||||
def __str__(self):
|
||||
return "Convert {} {}".format(self.bookid, self.kindle_mail)
|
||||
|
||||
+7
-7
@@ -158,10 +158,10 @@ class TaskEmail(CalibreTask):
|
||||
else:
|
||||
self.send_gmail_email(msg)
|
||||
except MemoryError as e:
|
||||
log.debug_or_exception(e)
|
||||
log.debug_or_exception(e, stacklevel=3)
|
||||
self._handleError(u'MemoryError sending e-mail: {}'.format(str(e)))
|
||||
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e:
|
||||
log.debug_or_exception(e)
|
||||
log.debug_or_exception(e, stacklevel=3)
|
||||
if hasattr(e, "smtp_error"):
|
||||
text = e.smtp_error.decode('utf-8').replace("\n", '. ')
|
||||
elif hasattr(e, "message"):
|
||||
@@ -171,11 +171,11 @@ class TaskEmail(CalibreTask):
|
||||
else:
|
||||
text = ''
|
||||
self._handleError(u'Smtplib Error sending e-mail: {}'.format(text))
|
||||
except socket.error as e:
|
||||
log.debug_or_exception(e)
|
||||
except (socket.error) as e:
|
||||
log.debug_or_exception(e, stacklevel=3)
|
||||
self._handleError(u'Socket Error sending e-mail: {}'.format(e.strerror))
|
||||
except Exception as ex:
|
||||
log.debug_or_exception(ex)
|
||||
log.debug_or_exception(ex, stacklevel=3)
|
||||
self._handleError(u'Error sending e-mail: {}'.format(ex))
|
||||
|
||||
def send_standard_email(self, msg):
|
||||
@@ -248,7 +248,7 @@ class TaskEmail(CalibreTask):
|
||||
data = file_.read()
|
||||
file_.close()
|
||||
except IOError as e:
|
||||
log.debug_or_exception(e)
|
||||
log.debug_or_exception(e, stacklevel=3)
|
||||
log.error(u'The requested file could not be read. Maybe wrong permissions?')
|
||||
return None
|
||||
# Set mimetype
|
||||
@@ -267,4 +267,4 @@ class TaskEmail(CalibreTask):
|
||||
return "E-mail"
|
||||
|
||||
def __str__(self):
|
||||
return "{}, {}".format(self.name, self.subject)
|
||||
return "E-mail {}, {}".format(self.name, self.subject)
|
||||
|
||||
@@ -32,3 +32,6 @@ class TaskUpload(CalibreTask):
|
||||
@property
|
||||
def name(self):
|
||||
return "Upload"
|
||||
|
||||
def __str__(self):
|
||||
return "Upload {}".format(self.message)
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
<th>{{_('Downloads')}}</th>
|
||||
<th class="hidden-xs ">{{_('Admin')}}</th>
|
||||
<th class="hidden-xs hidden-sm">{{_('Password')}}</th>
|
||||
{% if config.config_uploading %}
|
||||
<th class="hidden-xs hidden-sm">{{_('Upload')}}</th>
|
||||
{% endif %}
|
||||
<th class="hidden-xs hidden-sm">{{_('Download')}}</th>
|
||||
<th class="hidden-xs hidden-sm hidden-md">{{_('View Books')}}</th>
|
||||
<th class="hidden-xs hidden-sm hidden-md">{{_('Edit')}}</th>
|
||||
@@ -32,7 +34,9 @@
|
||||
<td>{{user.downloads.count()}}</td>
|
||||
<td class="hidden-xs">{{ display_bool_setting(user.role_admin()) }}</td>
|
||||
<td class="hidden-xs hidden-sm">{{ display_bool_setting(user.role_passwd()) }}</td>
|
||||
{% if config.config_uploading %}
|
||||
<td class="hidden-xs hidden-sm">{{ display_bool_setting(user.role_upload()) }}</td>
|
||||
{% endif %}
|
||||
<td class="hidden-xs hidden-sm">{{ display_bool_setting(user.role_download()) }}</td>
|
||||
<td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_viewer()) }}</td>
|
||||
<td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_edit()) }}</td>
|
||||
@@ -184,9 +188,8 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if feature_support['updater'] %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if feature_support['updater'] %}
|
||||
<div class="hidden" id="update_error"> <span>{{update_error}}</span></div>
|
||||
<div class="btn btn-primary" id="check_for_update">{{_('Check for Update')}}</div>
|
||||
<div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div>
|
||||
|
||||
+11
-11
@@ -5,11 +5,11 @@
|
||||
{% if author is not none %}
|
||||
<section class="author-bio">
|
||||
{%if author.image_url is not none %}
|
||||
<img title="{{author.name|safe}}" src="{{author.image_url}}" alt="{{author.name|safe}}" class="author-photo pull-left">
|
||||
<img title="{{author.name}}" src="{{author.image_url}}" alt="{{author.name}}" class="author-photo pull-left">
|
||||
{% endif %}
|
||||
|
||||
{%if author.about is not none %}
|
||||
<p>{{author.about|safe}}</p>
|
||||
<p>{{author.about}}</p>
|
||||
{% endif %}
|
||||
|
||||
- {{_("via")}} <a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">Goodreads</a>
|
||||
@@ -22,13 +22,13 @@
|
||||
{% if author is not none %}
|
||||
<h3>{{_("In Library")}}</h3>
|
||||
{% endif %}
|
||||
<div class="filterheader hidden-xs hidden-sm">
|
||||
<a id="new" data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a id="old" data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<a id="asc" data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a id="desc" data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a id="pub_new" data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a id="pub_old" data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" class="btn btn-primary" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<div class="filterheader hidden-xs">
|
||||
<a id="new" data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" class="btn btn-primary{% if order == "new" %} active{% endif%}" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a id="old" data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" class="btn btn-primary{% if order == "old" %} active{% endif%}" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<a id="asc" data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" class="btn btn-primary{% if order == "abc" %} active{% endif%}" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a id="desc" data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" class="btn btn-primary{% if order == "zyx" %} active{% endif%}" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a id="pub_new" data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" class="btn btn-primary{% if order == "pubnew" %} active{% endif%}" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a id="pub_old" data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" class="btn btn-primary{% if order == "pubold" %} active{% endif%}" href="{{url_for('web.books_list', data='author', book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
</div>
|
||||
<div class="row display-flex">
|
||||
{% if entries[0] %}
|
||||
@@ -36,7 +36,7 @@
|
||||
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||
<div class="cover">
|
||||
<a href="{{ url_for('web.show_book', book_id=entry.id) }}">
|
||||
<span class="img" title="{{entry.title|safe}}">
|
||||
<span class="img" title="{{entry.title}}">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" />
|
||||
{% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
|
||||
</span>
|
||||
@@ -98,7 +98,7 @@
|
||||
|
||||
{% if other_books and author is not none %}
|
||||
<div class="discover">
|
||||
<h3>{{_("More by")}} {{ author.name.replace('|',',')|safe }}</h3>
|
||||
<h3>{{_("More by")}} {{ author.name.replace('|',',') }}</h3>
|
||||
<div class="row">
|
||||
{% for entry in other_books %}
|
||||
<div class="col-sm-3 col-lg-2 col-xs-6 book">
|
||||
|
||||
@@ -98,8 +98,7 @@
|
||||
<label for="rating">{{_('Rating')}}</label>
|
||||
<input type="number" name="rating" id="rating" class="rating input-lg" data-clearable="" value="{% if book.ratings %}{{(book.ratings[0].rating / 2)|int}}{% endif %}">
|
||||
</div>
|
||||
{% if g.user.role_upload() or g.user.role_admin()%}
|
||||
{% if g.allow_upload %}
|
||||
{% if g.user.role_upload() and g.allow_upload %}
|
||||
<div class="form-group">
|
||||
<label for="cover_url">{{_('Fetch Cover from URL (JPEG - Image will be downloaded and stored in database)')}}</label>
|
||||
<input type="text" class="form-control" name="cover_url" id="cover_url" value="">
|
||||
@@ -109,7 +108,6 @@
|
||||
<div class="upload-cover-input-text" id="upload-cover"></div>
|
||||
<input id="btn-upload-cover" name="btn-upload-cover" type="file" accept=".jpg, .jpeg, .png, .webp">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<label for="pubdate">{{_('Published Date')}}</label>
|
||||
<div class="form-group input-group">
|
||||
@@ -197,14 +195,12 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if g.user.role_upload() or g.user.role_admin()%}
|
||||
{% if g.allow_upload %}
|
||||
{% if g.user.role_upload() and g.allow_upload %}
|
||||
<div role="group" aria-label="Upload new book format">
|
||||
<label class="btn btn-primary btn-file" for="btn-upload-format">{{ _('Upload Format') }}</label>
|
||||
<div class="upload-format-input-text" id="upload-format"></div>
|
||||
<input id="btn-upload-format" name="btn-upload-format" type="file">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="checkbox">
|
||||
@@ -230,7 +226,7 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="metaModalLabel">{{_('Fetch Metadata')}}</h4>
|
||||
<h4 class="modal-title text-center" id="metaModalLabel">{{_('Fetch Metadata')}}</h4>
|
||||
<form class="padded-bottom" id="meta-search">
|
||||
<div class="input-group">
|
||||
<label class="sr-only" for="keyword">{{_('Keyword')}}</label>
|
||||
@@ -251,7 +247,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
||||
<button id="meta_close" type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,17 +265,17 @@
|
||||
>
|
||||
<div class="media-body">
|
||||
<h4 class="media-heading">
|
||||
<a href="<%= url %>" target="_blank" rel="noopener"><%= title %></a>
|
||||
<a class="meta_title" href="<%= url %>" target="_blank" rel="noopener"><%= title %></a>
|
||||
</h4>
|
||||
<p>{{_('Author')}}:<%= authors.join(" & ") %></p>
|
||||
<p class="meta_author">{{_('Author')}}:<%= authors.join(" & ") %></p>
|
||||
<% if (publisher) { %>
|
||||
<p>{{_('Publisher')}}:<%= publisher %></p>
|
||||
<p class="meta_publisher">{{_('Publisher')}}:<%= publisher %></p>
|
||||
<% } %>
|
||||
<% if (description) { %>
|
||||
<p>{{_('Description')}}: <%= description %></p>
|
||||
<p class="meta_description">{{_('Description')}}: <%= description %></p>
|
||||
<% } %>
|
||||
<p>{{_('Source')}}:
|
||||
<a href="<%= source.url %>" target="_blank" rel="noopener"><%= source.description %></a>
|
||||
<a class="meta_source" href="<%= source.link %>" target="_blank" rel="noopener"><%= source.description %></a>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
<div class="text-left" id="merge_to"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<input type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||
<input id="merge_confirm" type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
|
||||
<button id="merge_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}>
|
||||
<label for="config_uploading">{{_('Enable Uploads')}}</label>
|
||||
<label for="config_uploading">{{_('Enable Uploads')}} {{_('(Please ensure users having also upload rights)')}}</label>
|
||||
</div>
|
||||
<div data-related="upload_settings">
|
||||
<div class="form-group">
|
||||
|
||||
@@ -95,17 +95,21 @@
|
||||
<input type="checkbox" name="viewer_role" id="viewer_role" {% if conf.role_viewer() %}checked{% endif %}>
|
||||
<label for="viewer_role">{{_('Allow eBook Viewer')}}</label>
|
||||
</div>
|
||||
{% if conf.config_uploading %}
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="upload_role" id="upload_role" {% if conf.role_upload() %}checked{% endif %}>
|
||||
<label for="upload_role">{{_('Allow Uploads')}}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="edit_role" id="edit_role" {% if conf.role_edit() %}checked{% endif %}>
|
||||
<input type="checkbox" name="edit_role" data-control="edit_settings" id="edit_role" {% if conf.role_edit() %}checked{% endif %}>
|
||||
<label for="edit_role">{{_('Allow Edit')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="delete_role" id="delete_role" {% if conf.role_delete_books() %}checked{% endif %}>
|
||||
<label for="delete_role">{{_('Allow Delete Books')}}</label>
|
||||
<div data-related="edit_settings">
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="delete_role" id="delete_role" {% if conf.role_delete_books() %}checked{% endif %}>
|
||||
<label for="delete_role">{{_('Allow Delete Books')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="passwd_role" id="passwd_role" {% if conf.role_passwd() %}checked{% endif %}>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
{% endif %}
|
||||
{% if g.user.kindle_mail and kindle_list %}
|
||||
{% if kindle_list.__len__() == 1 %}
|
||||
<a href="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=kindle_list[0]['format'], convert=kindle_list[0]['convert'])}}" id="sendbtn" data-text="{{_('Send to Kindle')}}" class="btn btn-primary" role="button"><span class="glyphicon glyphicon-send"></span> {{kindle_list[0]['text']}}</a>
|
||||
<div id="sendbtn" data-action="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=kindle_list[0]['format'], convert=kindle_list[0]['convert'])}}" data-text="{{_('Send to Kindle')}}" class="btn btn-primary postAction" role="button"><span class="glyphicon glyphicon-send"></span> {{kindle_list[0]['text']}}</div>
|
||||
{% else %}
|
||||
<div class="btn-group" role="group">
|
||||
<button id="sendbtn2" type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
@@ -47,7 +47,7 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="send-to-kindle">
|
||||
{% for format in kindle_list %}
|
||||
<li><a href="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{format['text']}}</a></li>
|
||||
<li><a class="postAction" data-action="{{url_for('web.send_to_kindle', book_id=entry.id, book_format=format['format'], convert=format['convert'])}}">{{format['text']}}</a></li>
|
||||
{%endfor%}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
{% extends "fragment.html" %}
|
||||
{% block body %}
|
||||
<div class="well">
|
||||
<p>
|
||||
{{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a>
|
||||
<p>
|
||||
{% if not warning %}
|
||||
{{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}
|
||||
</p><p>
|
||||
api_endpoint={{url_for("kobo.TopLevelEndpoint", auth_token=auth_token, _external=True)}}
|
||||
{% else %}
|
||||
{{warning}}
|
||||
</p><p>{{_('Kobo Token:')}} {{ auth_token }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% if not warning %}api_endpoint={{kobo_auth_url}}{% else %}{{warning}}{% endif %}</a>
|
||||
</p>
|
||||
<p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
+11
-12
@@ -2,23 +2,22 @@
|
||||
{% block body %}
|
||||
<h1 class="{{page}}">{{_(title)}}</h1>
|
||||
|
||||
<div class="filterheader hidden-xs hidden-sm">
|
||||
{% if entries.__len__() %}
|
||||
{% if entries[0][0].sort %}
|
||||
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
|
||||
{% endif %}
|
||||
<div class="filterheader hidden-xs">
|
||||
{% if entries.__len__() and data == 'author' %}
|
||||
<div id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></div>
|
||||
{% endif %}
|
||||
<button id="asc" data-id="series" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button>
|
||||
<button id="desc" data-id="series" data-order="{{ order }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div id="asc" data-id="series" data-order="{{ order }}" class="btn btn-primary{% if order == 1 %} active{% endif%}"><span class="glyphicon glyphicon-sort-by-alphabet"></span></div>
|
||||
<div id="desc" data-id="series" class="btn btn-primary{% if order == 0 %} active{% endif%}"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></div>
|
||||
{% if charlist|length %}
|
||||
<button id="all" class="btn btn-primary">{{_('All')}}</button>
|
||||
<div id="all" class="active btn btn-primary {% if charlist|length > 9 %}hidden-sm{% endif %}">{{_('All')}}</div>
|
||||
{% endif %}
|
||||
<div class="btn-group character" role="group">
|
||||
<div class="btn-group character {% if charlist|length > 9 %}hidden-sm{% endif %}" role="group">
|
||||
{% for char in charlist%}
|
||||
<button class="btn btn-primary char">{{char.char}}</button>
|
||||
<div class="btn btn-primary char">{{char.char}}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button class="update-view btn btn-primary" data-target="series_view" id="list-button" data-view="list">List</button>
|
||||
<div class="update-view btn btn-primary" data-target="series_view" id="list-button" data-view="list">List</div>
|
||||
</div>
|
||||
|
||||
{% if entries[0] %}
|
||||
@@ -28,7 +27,7 @@
|
||||
<div class="cover">
|
||||
<a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}">
|
||||
<span class="img" title="{{entry[0].series[0].name}}">
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry[0].id) }}" alt="{{ entry[0].name }}"/>
|
||||
<img src="{{ url_for('web.get_cover', book_id=entry[3]) }}" alt="{{ entry[0].series[0].name }}"/>
|
||||
<span class="badge">{{entry.count}}</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
+16
-11
@@ -64,19 +64,24 @@
|
||||
{% endif %}
|
||||
<div class="discover load-more">
|
||||
<h2 class="{{title}}">{{title}}</h2>
|
||||
<div class="filterheader hidden-xs hidden-sm">
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" id="asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" id="desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" id="auth_az" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='authaz')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" id="auth_za" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='authza')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" id="pub_new" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" id="pub_old" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<div class="filterheader hidden-xs">
|
||||
{% if page == 'hot' %}
|
||||
<a data-toggle="tooltip" title="{{_('Sort ascending according to download count')}}" id="hot_asc" class="btn btn-primary{% if order == "hotasc" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='hotasc')}}"><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort descending according to download count')}}" id="hot_desc" class="btn btn-primary{% if order == "hotdesc" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='hotdesc')}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
{% else %}
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary{% if order == "new" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary{% if order == "old" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" id="asc" class="btn btn-primary{% if order == "abc" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" id="desc" class="btn btn-primary{% if order == "zyx" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='zyx')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" id="auth_az" class="btn btn-primary{% if order == "authaz" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='authaz')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" id="auth_za" class="btn btn-primary{% if order == "authza" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='authza')}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" id="pub_new" class="btn btn-primary{% if order == "pubnew" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubnew')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" id="pub_old" class="btn btn-primary{% if order == "pubold" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='pubold')}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
{% if page == 'series' %}
|
||||
<a data-toggle="tooltip" title="{{_('Sort ascending according to series index')}}" id="series_asc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='seriesasc')}}"><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort descending according to series index')}}" id="series_desc" class="btn btn-primary" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='seriesdesc')}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort ascending according to series index')}}" id="series_asc" class="btn btn-primary{% if order == "seriesasc" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='seriesasc')}}"><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort descending according to series index')}}" id="series_desc" class="btn btn-primary{% if order == "seriesdesc" %} active{% endif%}" href="{{url_for('web.books_list', data=page, book_id=id, sort_param='seriesdesc')}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row display-flex">
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<h1>{{title}}</h1>
|
||||
<div class="filterheader hidden-xs">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div id="asc" data-order="{{ order }}" data-id="{{ data }}" class="btn btn-primary {% if order == 1 %} active{% endif%}"><span class="glyphicon glyphicon-sort-by-alphabet"></span></div>
|
||||
<div id="desc" data-id="{{ data }}" class="btn btn-primary{% if order == 0 %} active{% endif%}"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></div>
|
||||
{% if charlist|length %}
|
||||
<div id="all" class="active btn btn-primary {% if charlist|length > 9 %}hidden-sm{% endif %}">{{_('All')}}</div>
|
||||
{% endif %}
|
||||
<div class="btn-group character {% if charlist|length > 9 %}hidden-sm{% endif %}" role="group">
|
||||
{% for char in charlist%}
|
||||
<div class="btn btn-primary char">{{char}}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div div id="list" class="col-xs-12 col-sm-6">
|
||||
{% for lang in languages %}
|
||||
{% if loop.index0 == (loop.length/2)|int and loop.length > 20 %}
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
</div>
|
||||
<div id="second" class="col-xs-12 col-sm-6">
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{lang_counter[loop.index0].bookcount}}</span></div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{{url_for('web.books_list', book_id=lang.lang_code, data=data, sort_param='new')}}">{{lang.name}}</a></div>
|
||||
<div class="row" data-id="{{lang[0].name}}">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1" align="left"><span class="badge">{{lang[1]}}</span></div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-11"><a id="list_{{loop.index0}}" href="{{url_for('web.books_list', book_id=lang[0].lang_code, data=data, sort_param='new')}}">{{lang[0].name}}</a></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script src="{{ url_for('static', filename='js/filter_list.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -58,8 +58,7 @@
|
||||
{% endif %}
|
||||
<ul class="nav navbar-nav navbar-right" id="main-nav">
|
||||
{% if g.user.is_authenticated or g.allow_anonymous %}
|
||||
{% if g.user.role_upload() or g.user.role_admin()%}
|
||||
{% if g.allow_upload %}
|
||||
{% if g.user.role_upload() and g.allow_upload %}
|
||||
<li>
|
||||
<form id="form-upload" class="navbar-form" action="{{ url_for('editbook.upload') }}" data-title="{{_('Uploading...')}}" data-footer="{{_('Close')}}" data-failed="{{_('Error')}}" data-message="{{_('Upload done, processing, please wait...')}}" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
@@ -69,7 +68,6 @@
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if not g.user.is_anonymous %}
|
||||
<li><a id="top_tasks" href="{{url_for('web.get_tasks_status')}}"><span class="glyphicon glyphicon-tasks"></span> <span class="hidden-sm">{{_('Tasks')}}</span></a></li>
|
||||
@@ -185,7 +183,7 @@
|
||||
</div>
|
||||
<div class="modal-body">...</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
||||
<button type="button" id="details_close" class="btn btn-default" data-dismiss="modal">{{_('Close')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+9
-11
@@ -2,25 +2,23 @@
|
||||
{% block body %}
|
||||
<h1 class="{{page}}">{{_(title)}}</h1>
|
||||
|
||||
<div class="filterheader hidden-xs hidden-sm">
|
||||
{% if entries.__len__() %}
|
||||
{% if data == 'author' %}
|
||||
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
|
||||
{% endif %}
|
||||
<div class="filterheader hidden-xs">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
{% if entries.__len__() and data == 'author' %}
|
||||
<div id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></div>
|
||||
{% endif %}
|
||||
<button id="asc" data-order="{{ order }}" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button>
|
||||
<button id="desc" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button>
|
||||
<div id="asc" data-order="{{ order }}" data-id="{{ data }}" class="btn btn-primary {% if order == 1 %} active{% endif%}"><span class="glyphicon glyphicon-sort-by-alphabet"></span></div>
|
||||
<div id="desc" data-id="{{ data }}" class="btn btn-primary{% if order == 0 %} active{% endif%}"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></div>
|
||||
{% if charlist|length %}
|
||||
<button id="all" class="btn btn-primary">{{_('All')}}</button>
|
||||
<div id="all" class="active btn btn-primary {% if charlist|length > 9 %}hidden-sm{% endif %}">{{_('All')}}</div>
|
||||
{% endif %}
|
||||
<div class="btn-group character" role="group">
|
||||
<div class="btn-group character {% if charlist|length > 9 %}hidden-sm{% endif %}" role="group">
|
||||
{% for char in charlist%}
|
||||
<button class="btn btn-primary char">{{char.char}}</button>
|
||||
<div class="btn btn-primary char">{{char.char}}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if data == "series" %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="update-view btn btn-primary" data-target="series_view" id="grid-button" data-view="grid">Grid</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<body>
|
||||
<div id="sidebar">
|
||||
<div id="panels">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<!--input id="searchBox" placeholder="search" type="search"-->
|
||||
|
||||
<!--a id="show-Search" class="show_view icon-search" data-view="Search">Search</a-->
|
||||
|
||||
+11
-10
@@ -9,6 +9,7 @@
|
||||
{% if g.user.is_authenticated %}
|
||||
{% if g.user.shelf.all() or g.shelves_access %}
|
||||
<div id="shelf-actions" class="btn-toolbar" role="toolbar">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="btn-group" role="group" aria-label="Add to shelves">
|
||||
<button id="add-to-shelf" type="button" class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-list"></span> {{_('Add to shelf')}}
|
||||
@@ -17,7 +18,7 @@
|
||||
<ul id="add-to-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
|
||||
{% for shelf in g.shelves_access %}
|
||||
{% if not shelf.is_public or g.user.role_edit_shelfs() %}
|
||||
<li><a href="{{ url_for('shelf.search_to_shelf', shelf_id=shelf.id) }}"> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
|
||||
<li><a class="postAction" role="button" data-action="{{ url_for('shelf.search_to_shelf', shelf_id=shelf.id) }}"> {{shelf.name}}{% if shelf.is_public == 1 %} {{_('(Public)')}}{% endif %}</a></li>
|
||||
{% endif %}
|
||||
{%endfor%}
|
||||
</ul>
|
||||
@@ -25,15 +26,15 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="filterheader hidden-xs hidden-sm"><!-- ToDo: Implement filter for search results -->
|
||||
<a id="new" data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='new', query=query)}}"><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a id="old" data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='old', query=query)}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<a id="asc" data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='abc', query=query)}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a id="desc" data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='zyx', query=query)}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a id="auth_az" data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='authaz', query=query)}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a id="auth_za" data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='authza', query=query)}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a id="pub_new" data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='pubnew', query=query)}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a id="pub_old" data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" class="btn btn-primary" href="{{url_for('web.books_list', data=page, sort_param='pubold', query=query)}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<div class="filterheader hidden-xs"><!-- ToDo: Implement filter for search results -->
|
||||
<a id="new" data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" class="btn btn-primary{% if order == "new" %} active{% endif%}" href="{{url_for('web.books_list', data=page, sort_param='new', query=query)}}"><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a id="old" data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" class="btn btn-primary{% if order == "old" %} active{% endif%}" href="{{url_for('web.books_list', data=page, sort_param='old', query=query)}}"><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<a id="asc" data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" class="btn btn-primary{% if order == "abc" %} active{% endif%}" href="{{url_for('web.books_list', data=page, sort_param='abc', query=query)}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a id="desc" data-toggle="tooltip" title="{{_('Sort title in reverse alphabetical order')}}" class="btn btn-primary{% if order == "zyx" %} active{% endif%}" href="{{url_for('web.books_list', data=page, sort_param='zyx', query=query)}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a id="auth_az" data-toggle="tooltip" title="{{_('Sort authors in alphabetical order')}}" class="btn btn-primary{% if order == "authaz" %} active{% endif%}" href="{{url_for('web.books_list', data=page, sort_param='authaz', query=query)}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
<a id="auth_za" data-toggle="tooltip" title="{{_('Sort authors in reverse alphabetical order')}}" class="btn btn-primary{% if order == "authza" %} active{% endif%}" href="{{url_for('web.books_list', data=page, sort_param='authza', query=query)}}"><span class="glyphicon glyphicon-user"></span><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></a>
|
||||
<a id="pub_new" data-toggle="tooltip" title="{{_('Sort according to publishing date, newest first')}}" class="btn btn-primary{% if order == "pubnew" %} active{% endif%}" href="{{url_for('web.books_list', data=page, sort_param='pubnew', query=query)}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a id="pub_old" data-toggle="tooltip" title="{{_('Sort according to publishing date, oldest first')}}" class="btn btn-primary{% if order == "pubold" %} active{% endif%}" href="{{url_for('web.books_list', data=page, sort_param='pubold', query=query)}}"><span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -2,18 +2,20 @@
|
||||
{% block body %}
|
||||
<div class="discover">
|
||||
<h2>{{title}}</h2>
|
||||
<!--form method="post"--->
|
||||
{% if g.user.role_download() %}
|
||||
<a id="shelf_down" href="{{ url_for('shelf.show_simpleshelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
|
||||
{% endif %}
|
||||
{% if g.user.is_authenticated %}
|
||||
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="btn btn-danger" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="btn btn-danger" data-action="{{url_for('shelf.delete_shelf', shelf_id=shelf.id)}}" id="delete_shelf" data-value="{{ shelf.id }}">{{ _('Delete this Shelf') }}</div>
|
||||
<a id="edit_shelf" href="{{ url_for('shelf.edit_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Edit Shelf Properties') }} </a>
|
||||
</form>
|
||||
{% if entries.__len__() %}
|
||||
<a id="order_shelf" href="{{ url_for('shelf.order_shelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Arrange books manually') }} </a>
|
||||
<button id="toggle_order_shelf" type="button" data-alt-text="{{ _('Disable Change order') }}" class="btn btn-primary">{{ _('Enable Change order') }}</button>
|
||||
<div class="filterheader hidden-xs hidden-sm">
|
||||
<div class="filterheader hidden-xs">
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, newest first')}}" id="new" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='new')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort according to book date, oldest first')}}" id="old" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='old')}}"><span class="glyphicon glyphicon-book"></span> <span class="glyphicon glyphicon-calendar"></span><span class="glyphicon glyphicon-sort-by-order-alt"></span></a>
|
||||
<a data-toggle="tooltip" title="{{_('Sort title in alphabetical order')}}" id="asc" class="btn btn-primary disabled" href="{{url_for('shelf.show_shelf', shelf_id=shelf.id, sort_param='abc')}}"><span class="glyphicon glyphicon-font"></span><span class="glyphicon glyphicon-sort-by-alphabet"></span></a>
|
||||
@@ -84,22 +86,6 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<!--div id="DeleteShelfDialog" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-center">
|
||||
<span>{{_('Are you sure you want to delete this shelf?')}}</span>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<span>{{_('Shelf will be deleted for all users')}}</span>
|
||||
<p></p>
|
||||
<a id="confirm" href="{{ url_for('shelf.delete_shelf', shelf_id=shelf.id) }}" class="btn btn-danger">{{_('OK')}}</a>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div-->
|
||||
|
||||
{% endblock %}
|
||||
{% block modal %}
|
||||
{{ delete_confirm_modal() }}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
{% if ( g.user and g.user.role_passwd() or g.user.role_admin() ) and not content.role_anonymous() %}
|
||||
{% if g.user and g.user.role_admin() and not new_user and not profile and ( mail_configured and content.email if content.email != None ) %}
|
||||
<a class="btn btn-default" id="resend_password" href="{{url_for('admin.reset_user_password', user_id = content.id) }}">{{_('Reset user Password')}}</a>
|
||||
<a class="btn btn-default postAction" id="resend_password" role="button" data-action="{{url_for('admin.reset_user_password', user_id = content.id) }}">{{_('Reset user Password')}}</a>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<label for="password">{{_('Password')}}</label>
|
||||
@@ -66,6 +66,9 @@
|
||||
<a class="btn btn-default" id="config_create_kobo_token" data-toggle="modal" data-target="#modal_kobo_token" data-remote="false" href="{{ url_for('kobo_auth.generate_auth_token', user_id=content.id) }}">{{_('Create/View')}}</a>
|
||||
<div class="btn btn-danger" id="config_delete_kobo_token" data-value="{{ content.id }}" data-remote="false" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Delete')}}</div>
|
||||
</div>
|
||||
<div class="form-group col">
|
||||
<div class="btn btn-default" id="kobo_full_sync" data-value="{{ content.id }}" {% if not content.remote_auth_token.first() %} style="display: none;" {% endif %}>{{_('Force full kobo sync')}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-sm-6">
|
||||
{% for element in sidebar %}
|
||||
@@ -101,29 +104,33 @@
|
||||
<input type="checkbox" name="viewer_role" id="viewer_role" {% if content.role_viewer() %}checked{% endif %}>
|
||||
<label for="viewer_role">{{_('Allow eBook Viewer')}}</label>
|
||||
</div>
|
||||
{% if config.config_uploading %}
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="upload_role" id="upload_role" {% if content.role_upload() %}checked{% endif %}>
|
||||
<label for="upload_role">{{_('Allow Uploads')}}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="edit_role" id="edit_role" {% if content.role_edit() %}checked{% endif %}>
|
||||
<input type="checkbox" name="edit_role" data-control="edit_settings" id="edit_role" {% if content.role_edit() %}checked{% endif %}>
|
||||
<label for="edit_role">{{_('Allow Edit')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="delete_role" id="delete_role" {% if content.role_delete_books() %}checked{% endif %}>
|
||||
<label for="delete_role">{{_('Allow Delete Books')}}</label>
|
||||
<div data-related="edit_settings">
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="delete_role" id="delete_role" {% if content.role_delete_books() %}checked{% endif %}>
|
||||
<label for="delete_role">{{_('Allow Delete Books')}}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% if not content.role_anonymous() %}
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}>
|
||||
<label for="passwd_role">{{_('Allow Changing Password')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="edit_shelf_role" id="edit_shelf_role" {% if content.role_edit_shelfs() %}checked{% endif %}>
|
||||
<label for="edit_shelf_role">{{_('Allow Editing Public Shelves')}}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not content.role_anonymous() %}
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}>
|
||||
<label for="passwd_role">{{_('Allow Changing Password')}}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="edit_shelf_role" id="edit_shelf_role" {% if content.role_edit_shelfs() %}checked{% endif %}>
|
||||
<label for="edit_shelf_role">{{_('Allow Editing Public Shelves')}}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if kobo_support and not content.role_anonymous() %}
|
||||
<div class="form-group">
|
||||
<input type="checkbox" name="kobo_only_shelves_sync" id="kobo_only_shelves_sync" {% if content.kobo_only_shelves_sync %}checked{% endif %}>
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user