Compare commits

..
141 Commits
Author SHA1 Message Date
Ozzie Isaacs e0e0422010 Upates for new release 2022-01-24 19:18:40 +01:00
Ozzie Isaacs 01ab75a158 Updated requirements for gdrive and gmail
Updated teststatus
2022-01-24 18:47:34 +01:00
Ozzie Isaacs d9c10b830a Added variable to allow loading cover from localhost 2022-01-24 18:47:34 +01:00
Ozzie Isaacs 35f6f4c727 Deleted book formats remove book from synced to kobo table
updated teststatus
2022-01-24 18:47:34 +01:00
Ozzie Isaacs 3b216bfa07 Kobo sync token is now also created if accessed from localhost(fixes #1990)
Create kobo sync token button is now "unclicked" after closing dialog
Additional localhost route is catched
If book format is deleted this also deletes the book synced to kobo status
2022-01-24 18:47:34 +01:00
Ozzieisaacs e8e2f789e5 Fix #2287 (books can't be added to shelf from search) 2022-01-24 12:37:15 +01:00
Ozzie Isaacs bbef41290f Fix delete book format wasn't working 2022-01-22 14:01:32 +01:00
Ozzie Isaacs 81b85445d8 Added missing unique marker on comments table (#2278) 2022-01-22 11:51:34 +01:00
Ozzie Isaacs 35209ede67 Update cover extraction with comicapi for webp files (fixes #2280) 2022-01-22 10:31:18 +01:00
Ozzie Isaacs 0c0313f375 Prevent creating a public shelf without permission 2022-01-18 17:55:10 +01:00
Ozzie Isaacs 6bf0753978 Prevent wrong use of safe statement 2022-01-18 17:39:54 +01:00
Ozzie Isaacs a02f621f08 Added id for testability 2022-01-17 18:38:37 +01:00
Ozzie Isaacs de1bc3f9af Fix #2265 (add book to shelf on popup for book details)
Updated testresults
2022-01-17 17:46:57 +01:00
Ozzie Isaacs 01090169a7 Version bump
update dependencies in setup.cfg
Update security bug list
2022-01-16 12:55:15 +01:00
Ozzie Isaacs b564a97cdf Update version
Update testresult
2022-01-16 12:06:02 +01:00
Ozzie Isaacs a118fffc99 Added missing chardet (used by requests in older versions) 2022-01-15 19:58:13 +01:00
Ozzie Isaacs 5b59aab81a Udpate requirements and config.cfg 2022-01-15 18:34:21 +01:00
Ozzie Isaacs 7d3d0c661e Merge remote-tracking branch 'fr/master' 2022-01-15 09:19:55 +01:00
Ozzie Isaacs f6f20ebc77 Update teststatus 2022-01-15 09:18:51 +01:00
Thomas 58cb54c76f small typo 2022-01-13 22:47:47 +01:00
Thomas 3fc326fa48 Update French translation 2022-01-13 22:45:16 +01:00
Ozzie Isaacs a50aff67a2 Merge remote-tracking branch 'korean/master' 2022-01-10 17:38:09 +01:00
Ozzie Isaacs 3c1f5fd37f Update Test results 2022-01-10 17:35:18 +01:00
byword77 8ae066c387 Add files via upload 2022-01-10 11:59:16 +09:00
Ozzie Isaacs 0feb62c142 Improvements delete user (delete also depending entries in tables)
Fixes kobo sync with sync only selected shelfs
2022-01-09 17:43:20 +01:00
Ozzie Isaacs 96b1e8960b Bugfix send to kindle with multiple formats 2022-01-09 12:55:38 +01:00
Ozzie Isaacs df67079573 Fixes for kobosync with multiple users (#2230) 2022-01-08 19:03:59 +01:00
Ozzie Isaacs e3dbf7a88d Bugfixes load metadata
Improvements for testing load metadata
2022-01-07 12:40:03 +01:00
byword77 8dd0585919 Add files via upload 2022-01-07 11:07:19 +09:00
Ozzie Isaacs c830a5936e Added dse to supported languages 2022-01-06 14:10:06 +01:00
Ozzie Isaacs 405f3c181f Added korean locale 2022-01-06 14:04:49 +01:00
Ozzie Isaacs 6d839d5cc7 Bugfixes from testrun 2022-01-04 21:12:59 +01:00
Ozzieisaacs bbadfa2251 bugfixes load metadata 2022-01-04 21:11:52 +01:00
Ozzieisaacs 7b8b2f93a0 Merge remote-tracking branch 'scholary/master' 2021-12-30 14:55:39 +01:00
Ozzieisaacs c1030dfd13 Update dependency scholary 2021-12-30 14:45:31 +01:00
Ozzieisaacs a90177afa0 Better version output in about page (exe file, pyPi, git commit string removed if empty) 2021-12-30 14:45:31 +01:00
Ozzieisaacs c095ee3c14 Fix #2243 (whitespaces are trimmed also for normal search) 2021-12-30 14:45:31 +01:00
Ozzieisaacs ae1f515446 Bugfix uncheck all ekements in books list and user list
Improved testability for books list
2021-12-30 14:45:31 +01:00
Ozzieisaacs 0548fbb685 Bugfix post commands without updater 2021-12-30 14:45:31 +01:00
Ozzieisaacs f22e4d996c Proxy kobo library sync at the end of local sync 2021-12-30 14:45:31 +01:00
Ozzieisaacs 3e0d8763c3 Prevent 2 public shelfs with same names due to changing public property 2021-12-30 14:45:31 +01:00
Ozzieisaacs 47f5e2ffb4 Remove python2 urllib imports
Fix for "javascript:" script links in identifier
2021-12-30 14:45:31 +01:00
Ozzieisaacs f39dc100b4 Migrated some routes to POST
- shelf massadd
- resetpassword
- delete shelf
- send to kindle
2021-12-30 14:45:31 +01:00
Ozzieisaacs 573c9f9fb4 Improved logging (right stacklevel on mail exceptions)
Updated jsonschema requirements
2021-12-30 14:45:31 +01:00
Ozzieisaacs 785726deee Migrated some routes to POST
- delete shelf, import ldap users
- delete_kobo token, kobo force full sync
- shutdown, reconnect, shutdown
2021-12-30 14:45:30 +01:00
cbartondock 4edd1914b4 Fixed google scholar issues 2021-12-23 23:16:41 -05:00
cbartondock 222929e741 Fixed extra s in scholar metadata 2021-12-23 18:05:20 -05:00
cbartondock 70b67077cc Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-23 15:58:45 -05:00
Ozzie Isaacs ec73558b03 Bugfix advanced search for language
Update results from testrun
2021-12-23 19:18:09 +01:00
Ozzieisaacs bdedec90dd Catch more Gdrive errors (#2233) 2021-12-23 11:13:08 +01:00
Ozzieisaacs d45085215f Update Translation 2021-12-22 19:08:15 +01:00
Ozzie Isaacs b85627da5c Fix for adding/deleting visibility restrictions on custom columns/tags 2021-12-19 11:29:54 +01:00
Ozzie Isaacs 592288cb22 Fix inverted "convert to English characters" setting 2021-12-19 08:04:13 +01:00
Ozzie Isaacs 2e3a3ee460 Update dependencies in setup.cfg 2021-12-19 07:28:46 +01:00
Ozzie Isaacs b7927a0df1 Bugfix handling of stacktrace from calibre 2021-12-18 18:38:17 +01:00
Ozzie Isaacs 021298374e Improved debug output for task added
Improved handling of calibre output on windows
2021-12-18 17:31:33 +01:00
Ozzie Isaacs 92f65882b2 Added log4j statement 2021-12-16 06:21:16 +01:00
Ozzie Isaacs f0399d04b7 Merge remote-tracking branch 'bookmark_csrf/master' 2021-12-14 17:59:32 +01:00
Ozzie Isaacs 9b57fa25de Merge remote-tracking branch 'zh/master' 2021-12-14 17:52:12 +01:00
Ozzie Isaacs afbe77de3d Merge remote-tracking branch 'border/patch-1' 2021-12-14 17:50:02 +01:00
xlivevil d26d357151 Merge branch 'master' into master 2021-12-15 00:17:26 +08:00
Laurin Neff 4db9691cfc Remove white border in full-screen cover preview 2021-12-13 19:36:21 +01:00
Ozzie Isaacs 45c433caab Added "hmj" to supported languages. Fix for "bit" language support (Fix #2217) 2021-12-13 18:26:47 +01:00
Ozzie Isaacs 25422b3411 Fix for #2195 Sync token only updated after complete library sync and sync token content is discarded on first sync to enable real forced full sync 2021-12-12 20:31:45 +01:00
xlivevil a184f4e71a Merge branch 'master' into master 2021-12-08 22:58:30 +08:00
cbartondock 4569188008 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-07 08:37:27 -05:00
Ozzie Isaacs 9d9acb058d Add button to force full kobo sync 2021-12-06 21:02:06 +01:00
Ozzie Isaacs 7d67168a4a Update test result 2021-12-06 20:27:25 +01:00
cbartondock 09751d8b87 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-05 19:00:15 -05:00
Ozzie Isaacs fd5ab0ef53 Bugfix handle archive bit 2021-12-05 18:01:56 +01:00
Ozzie Isaacs 3bf173d958 Added response for kobo-benefits route and kobo-gettest route 2021-12-04 15:44:41 +01:00
cbartondock 98d630d453 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-04 09:21:01 -05:00
Ozzie Isaacs eb2e816bfd Switch encoding in kobo metadata to ensure utf-8 characters to show up properly (finally) 2021-12-04 14:58:28 +01:00
Ozzie Isaacs bd01e840ca Delete books in shelfs, downloaded books, kobo sync status, etc on database change (fixes #620) 2021-12-04 11:50:25 +01:00
Ozzie Isaacs 91a21ababe Allow download of archived books 2021-12-04 11:16:33 +01:00
Ozzie Isaacs f4096b136e Added chinese sign language (csl) to supported languages 2021-12-04 10:57:53 +01:00
cbartondock 7598dfe952 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-12-01 16:17:14 -05:00
Ozzie Isaacs 5ed3b1cf53 On master: Order of language count in /language (fixes #2200) 2021-12-01 21:38:43 +01:00
Ozzie Isaacs 7640ac1b3b Books are removed from synced books upon archiving (from kobo or calibre-web side)
unicode texts (title, author) are showing up right on kobo reader
Added some missing kobo routes (prevents 404 response)
Added a lot of debug output on kobo sync requests
2021-12-01 20:29:51 +01:00
Jonathan Fenske 66874f8163 Update read.html
include the CSR token input
2021-11-30 22:25:45 -06:00
Jonathan Fenske 3f91313303 Update epub.js
send the CSRF token when adding bookmarks
2021-11-30 22:24:34 -06:00
xlivevil 8438b2a07b Update Simplified Chinese translation 2021-12-01 12:14:57 +08:00
xlivevil 67e3721530 fix(tinymce Simplified Chinese locale):rename zh_CN to zh_Hans_CN 2021-12-01 11:28:03 +08:00
cbartondock 2252d661c0 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-27 14:06:36 -05:00
Ozzie Isaacs 87e526642c Bugfix edit series_index
Bugfix invalid languages
2021-11-27 18:23:06 +01:00
Ozzie Isaacs 7f9da94a18 Tie visibility of upload buttons to upload right only 2021-11-27 17:43:51 +01:00
Ozzie Isaacs ec7c2db971 Added package variable for generating "exe" file with pyinstaller 2021-11-27 12:26:35 +01:00
cbartondock fcd2b68359 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-23 13:59:43 -05:00
Ozzie Isaacs a1d372630d Added missing language bit (fixes #2187)
Added handling of wrong .jpg cover file content
Guest sorting options are now stored in the session
Updated teststatus
2021-11-23 19:32:48 +01:00
cbartondock fc859afb92 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-22 09:59:36 -05:00
Ozzie Isaacs 2e0d0a2429 Update requirements 2021-11-21 18:31:35 +01:00
Ozzie Isaacs d084a06e63 Bugfix create shelf for users not allowed to create public shelfs 2021-11-21 16:21:27 +01:00
Ozzie Isaacs e880238cb9 Add missing filter for current user in KoboSyncedBooks queries 2021-11-21 16:19:32 +01:00
cbartondock f58c5bee1c Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-21 09:23:52 -05:00
Ozzie Isaacs cbb9edac19 Bugfix search in books list
Fix renaming upper to lowercase letters
Update test results
2021-11-21 13:23:34 +01:00
Ozzie Isaacs 1b8bd27b3c Added cve number for csrf bug 2021-11-21 12:35:53 +01:00
Ozzieisaacs 6cb713d62c Added filtering of languages
Bugfix show all allowed languages in user settings in case restrictions currently apply
2021-11-21 10:21:45 +01:00
Ozzieisaacs 642af2f973 Added support for missing locale Enu 2021-11-21 09:28:03 +01:00
Ozzieisaacs 9273843062 Removed double declared functions 2021-11-21 09:14:16 +01:00
cbartondock 2989586c6d Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-20 18:54:38 -05:00
Ozzie Isaacs 5ede079401 Handling of invalid cover files on upload 2021-11-20 13:45:41 +01:00
Ozzie Isaacs 7ad419dc8c Fix upload of cover and book formats containing html characters 2021-11-20 13:40:23 +01:00
Ozzie Isaacs bcdc976414 Added missing check for creating public shelfs 2021-11-20 13:00:54 +01:00
Ozzie Isaacs 6aad9378b8 Fix visiblility upload right on enabled upload feature 2021-11-20 12:44:34 +01:00
Ozzie Isaacs 6f5390ead5 Changed error message in case of trying to delete a shelf unauthorized
Removed outcommented text
2021-11-20 12:17:03 +01:00
Ozzie Isaacs d624b67e93 Fix #2174 (default sorting order now is timestamp again) 2021-11-20 11:57:51 +01:00
Ozzie Isaacs 01cc97c1b2 Added langugae support for Cholón (fixes #2183) 2021-11-20 11:32:51 +01:00
cbartondock 7c016265d2 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-10 08:59:23 -05:00
Ozzie Isaacs 974549b1af Removed technosoft2000 docker container 2021-11-08 20:36:41 +04:00
Ozzieisaacs add502d236 Fix opds search and opds list of read books 2021-11-07 20:18:33 +04:00
cbartondock 294c594cbb Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-06 14:19:37 -04:00
Ozzieisaacs 60aa016734 Handling of missing required dependency during dependency check 2021-11-06 21:17:48 +04:00
Ozzieisaacs 27e8fbd248 Show dependencies in about section from automatic dependency check
Bugfix and refactored dependency check
Added output of googledrive dependencies as fallback option
2021-11-06 20:58:51 +04:00
cbartondock d856c4d78e Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-04 14:00:51 -04:00
Ozzieisaacs 25b09a532f Fixed missing handle_error in convert calibre task (database readonly case) 2021-11-04 21:23:02 +04:00
Ozzieisaacs c1f4ca36b6 Current sorting order visible in all sidebar selectors, and search results
Sorting "hot" books only ascending and descending according to download numbers
Downloaded books sorting according to authors name working
2021-11-04 20:51:48 +04:00
Ozzieisaacs 58379159fb Fix shown cover in series grid view (#979) 2021-11-04 17:32:31 +04:00
Ozzieisaacs 17470b3b56 Check versions of dependencies at startup and generate logfile output if not fitting (#2157) 2021-11-04 16:38:55 +04:00
Ozzieisaacs 42cc13d1e2 Mark which functions are selected on list pages 2021-11-03 21:07:16 +04:00
Ozzieisaacs ecc5cb167e Upload setting in user template only visible if upload enabled
Delete book setting only visible if edit book setting is ticked
2021-11-03 20:32:17 +04:00
Ozzieisaacs d72210c6ae Exclude upload rights visibility if upload is not activated 2021-11-03 19:05:24 +04:00
Ozzieisaacs 61deda1076 Updated security history 2021-11-03 18:29:30 +04:00
Ozzieisaacs 1e0ff0f9c2 Fix #2045 ("fetch metadata" update appends to the existing tags instead or replacing them) 2021-11-03 18:16:15 +04:00
cbartondock 00bf1f5ec9 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-02 14:25:46 -04:00
Ozzieisaacs 2b6f5b1565 Fix csrf check in list pages 2021-11-02 21:32:29 +04:00
cbartondock e5abc5f281 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-02 13:09:59 -04:00
Ozzieisaacs 95371d0d7f Fix shelf grid ordering for inverted order at page load
Ordering buttons now visible on medium size screens
2021-11-02 20:27:50 +04:00
Ozzieisaacs c7df8a1a34 Fix missing csrf token in series grid view 2021-11-02 18:57:28 +04:00
Ozzieisaacs b414d91964 Fix for #2162 (Epub viewer isn't displaying images) 2021-11-02 18:55:56 +04:00
cbartondock 82164fc8e7 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-11-01 10:00:18 -04:00
Ozzieisaacs a5415e00d5 Fix grid to list in series view
Fix sort asc, desc in author and series list
2021-11-01 13:11:49 +01:00
cbartondock 434029270b Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-31 20:42:21 -04:00
Ozzie Isaacs b3b85bf692 Fix position read mark in standard theme 2021-10-31 16:33:35 +01:00
Ozzie Isaacs a36c6da3ae Fix position read mark in standard theme 2021-10-31 16:30:51 +01:00
Ozzie Isaacs 9e72c3b40d Fix position read mark in standard theme 2021-10-31 16:18:27 +01:00
cbartondock a0cebfbfb1 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-31 08:29:27 -04:00
Ozzie Isaacs 64e833f5d6 Updated readme
Version Bump
2021-10-31 12:00:20 +01:00
cbartondock 99ec99b539 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-30 13:28:56 -04:00
cbartondock 75c68d92ec Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-26 09:58:36 -04:00
cbartondock 27f71d910b Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-24 18:59:10 -04:00
cbartondock 88278672d8 Merge branch 'master' of https://github.com/janeczku/calibre-web 2021-10-23 09:45:11 -04:00
112 changed files with 13962 additions and 11636 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ If applicable, add screenshots to help explain your problem.
- OS: [e.g. Windows 10/Raspberry Pi OS] - OS: [e.g. Windows 10/Raspberry Pi OS]
- Python version: [e.g. python2.7] - Python version: [e.g. python2.7]
- Calibre-Web version: [e.g. 0.6.8 or 087c4c59 (git rev-parse --short HEAD)]: - 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] - Special Hardware: [e.g. Rasperry Pi Zero]
- Browser: [e.g. Chrome 83.0.4103.97, Safari 13.3.7, Firefox 68.0.1 ESR] - Browser: [e.g. Chrome 83.0.4103.97, Safari 13.3.7, Firefox 68.0.1 ESR]
+9 -6
View File
@@ -19,7 +19,7 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
- full graphical setup - full graphical setup
- User management with fine-grained per-user permissions - User management with fine-grained per-user permissions
- Admin interface - Admin interface
- User Interface in 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 - OPDS feed for eBook reader apps
- Filter and search by titles, authors, tags, series and language - Filter and search by titles, authors, tags, series and language
- Create a custom book collection (shelves) - Create a custom book collection (shelves)
@@ -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 - "Magic Link" login to make it easy to log on eReaders
- Login via LDAP, google/github oauth and via proxy authentication - 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`). 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 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` 3. Calibre-Web can be started afterwards by typing `cps` or `python3 -m cps`
@@ -48,17 +48,20 @@ Calibre-Web is a web app providing a clean interface for browsing, reading and d
1. Install dependencies by running `pip3 install --target vendor -r requirements.txt` (python3.x). Alternativly set up a python virtual environment. 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) 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 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\ 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) 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 Go to Login page
**Default admin login:**\ #### Default admin login:
*Username:* admin\ *Username:* admin\
*Password:* admin123 *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 ## Requirements
+24 -14
View File
@@ -11,19 +11,29 @@ To receive fixes for security vulnerabilities it is required to always upgrade t
## History ## History
| Fixed in | Description |CVE number | | Fixed in | Description |CVE number |
| ---------- |---------|---------| |---------------|--------------------------------------------------------------------------------------------------------------------|---------|
| 3rd July 2018 | Guest access acts as a backdoor|| | 3rd July 2018 | Guest access acts as a backdoor ||
| V 0.6.7 |Hardcoded secret key for sessions |CVE-2020-12627 | | 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 | 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 | 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 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 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 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 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 description series, categories or publishers title ||
| V 0.6.13|JavaScript could get executed in the shelf 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.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 | CSRF was possible. Thanks to @mik317 and Hagai Wechsler (WhiteSource) |CVE-2021-25965|
| V 0.6.14|Cross-Site Scripting vulnerability on typeahead inputs. Thanks to @notdodo|| | 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
View File
@@ -19,6 +19,7 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
__package__ = "cps"
import sys import sys
import os import os
@@ -35,6 +36,7 @@ from flask_principal import Principal
from . import config_sql, logger, cache_buster, cli, ub, db from . import config_sql, logger, cache_buster, cli, ub, db
from .reverseproxy import ReverseProxied from .reverseproxy import ReverseProxied
from .server import WebServer from .server import WebServer
from .dep_check import dependency_check
try: try:
import lxml import lxml
@@ -100,6 +102,7 @@ _BABEL_TRANSLATIONS = set()
log = logger.create() log = logger.create()
from . import services from . import services
db.CalibreDB.update_config(config) 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" ***') print('*** "flask-WTF" is needed for calibre-web to run. Please install it using pip: "pip install flask-WTF" ***')
web_server.stop(True) web_server.stop(True)
sys.exit(7) 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) app.wsgi_app = ReverseProxied(app.wsgi_app)
if os.environ.get('FLASK_DEBUG'): if os.environ.get('FLASK_DEBUG'):
+36 -10
View File
@@ -33,8 +33,9 @@ try:
except ImportError: except ImportError:
flaskwtf_version = _(u'not installed') 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 from .render_template import render_title_template
try: try:
from flask_login import __version__ as flask_loginVersion from flask_login import __version__ as flask_loginVersion
except ImportError: except ImportError:
@@ -67,13 +68,28 @@ from . import services
about = flask.Blueprint('about', __name__) 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( 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()), Platform = '{0[0]} {0[2]} {0[3]} {0[4]} {0[5]}'.format(platform.uname()),
Python=sys.version, Python=sys.version,
Calibre_Web=constants.STABLE_VERSION['version'] + ' - ' Calibre_Web=calibre_web_version,
+ constants.NIGHTLY_VERSION[0].replace('%','%%') + ' - '
+ constants.NIGHTLY_VERSION[1].replace('%','%%'),
WebServer=server.VERSION, WebServer=server.VERSION,
Flask=flask.__version__, Flask=flask.__version__,
Flask_Login=flask_loginVersion, Flask_Login=flask_loginVersion,
@@ -96,9 +112,21 @@ _VERSIONS = OrderedDict(
jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None, jsonschema=services.SyncToken.__version__ if bool(services.SyncToken) else None,
flask_dance=flask_danceVersion, flask_dance=flask_danceVersion,
greenlet=greenlet_Version greenlet=greenlet_Version
) )
_VERSIONS.update(uploader.get_versions()) _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(): def collect_stats():
_VERSIONS['ebook converter'] = _(converter.get_calibre_version()) _VERSIONS['ebook converter'] = _(converter.get_calibre_version())
@@ -115,5 +143,3 @@ def stats():
series = calibre_db.session.query(db.Series).count() series = calibre_db.session.query(db.Series).count()
return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(), return render_title_template('stats.html', bookcounter=counter, authorcounter=authors, versions=collect_stats(),
categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat") categorycounter=categorys, seriecounter=series, title=_(u"Statistics"), page="stat")
+61 -17
View File
@@ -129,11 +129,11 @@ def admin_forbidden():
abort(403) abort(403)
@admi.route("/shutdown") @admi.route("/shutdown", methods=["POST"])
@login_required @login_required
@admin_required @admin_required
def shutdown(): def shutdown():
task = int(request.args.get("parameter").strip()) task = request.get_json().get('parameter', -1)
showtext = {} showtext = {}
if task in (0, 1): # valid commandos received if task in (0, 1): # valid commandos received
# close all database connections # 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)?') texts["main"] = _('Are you sure you want to change shelf sync behavior for the selected user(s)?')
elif element_id == "db_submit": elif element_id == "db_submit":
texts["main"] = _('Are you sure you want to change Calibre library location?') 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) return json.dumps(texts)
@@ -754,7 +756,12 @@ def prepare_tags(user, action, tags_name, id_list):
return ",".join(saved_tags_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']) @admi.route("/ajax/addrestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required @login_required
@admin_required @admin_required
@@ -801,7 +808,13 @@ def add_restriction(res_type, user_id):
return "" 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']) @admi.route("/ajax/deleterestriction/<int:res_type>/<int:user_id>", methods=['POST'])
@login_required @login_required
@admin_required @admin_required
@@ -889,10 +902,18 @@ def list_restriction(res_type, user_id):
else: else:
json_dumps = "" json_dumps = ""
js = json.dumps(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" response.headers["Content-Type"] = "application/json; charset=utf-8"
return response 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/") @admi.route("/ajax/pathchooser/")
@login_required @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): 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'), return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
gdrive_error) 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") _config_string(to_save, "config_calibre_dir")
calibre_db.update_config(config) calibre_db.update_config(config)
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK): if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
flash(_(u"DB is not Writeable"), category="warning") flash(_(u"DB is not Writeable"), category="warning")
# warning = {'type': "warning", 'message': _(u"DB is not Writeable")}
config.save() config.save()
return _db_configuration_result(None, gdrive_error) 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")) raise Exception(_(u"E-mail is not from valid domain"))
except Exception as ex: except Exception as ex:
flash(str(ex), category="error") flash(str(ex), category="error")
return render_title_template("user_edit.html", 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", languages=languages, title=_(u"Add new user"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check) kobo_support=kobo_support, registered_oauth=oauth_check)
try: try:
@@ -1383,16 +1415,25 @@ def _delete_user(content):
for us in ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id): for us in ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id):
ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete() ub.session.query(ub.BookShelf).filter(us.id == ub.BookShelf.shelf).delete()
ub.session.query(ub.Shelf).filter(content.id == ub.Shelf.user_id).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.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() ub.session_commit()
log.info(u"User {} deleted".format(content.name)) log.info("User {} deleted".format(content.name))
return(_(u"User '%(nick)s' deleted", nick=content.name)) return(_("User '%(nick)s' deleted", nick=content.name))
else: else:
log.warning(_(u"Can't delete Guest User")) log.warning(_("Can't delete Guest User"))
raise Exception(_(u"Can't delete Guest User")) raise Exception(_("Can't delete Guest User"))
else: else:
log.warning(u"No admin user remaining, can't delete user") log.warning("No admin user remaining, can't delete user")
raise Exception(_(u"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): 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, kobo_support=kobo_support,
new_user=0, new_user=0,
content=content, content=content,
config=config,
registered_oauth=oauth_check, registered_oauth=oauth_check,
title=_(u"Edit User %(nick)s", nick=content.name), title=_(u"Edit User %(nick)s", nick=content.name),
page="edituser") page="edituser")
@@ -1496,7 +1538,8 @@ def new_user():
content.sidebar_view = config.config_default_show content.sidebar_view = config.config_default_show
content.locale = config.config_default_locale content.locale = config.config_default_locale
content.default_language = config.config_default_language 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", languages=languages, title=_(u"Add new user"), page="newuser",
kobo_support=kobo_support, registered_oauth=oauth_check) kobo_support=kobo_support, registered_oauth=oauth_check)
@@ -1571,7 +1614,7 @@ def edit_user(user_id):
if not content or (not config.config_anonbrowse and content.name == "Guest"): if not content or (not config.config_anonbrowse and content.name == "Guest"):
flash(_(u"User not found"), category="error") flash(_(u"User not found"), category="error")
return redirect(url_for('admin.admin')) return redirect(url_for('admin.admin'))
languages = calibre_db.speaking_language() languages = calibre_db.speaking_language(return_all_languages=True)
translations = babel.list_translations() + [LC('en')] translations = babel.list_translations() + [LC('en')]
kobo_support = feature_support['kobo'] and config.config_kobo_sync kobo_support = feature_support['kobo'] and config.config_kobo_sync
if request.method == "POST": if request.method == "POST":
@@ -1584,6 +1627,7 @@ def edit_user(user_id):
languages=languages, languages=languages,
new_user=0, new_user=0,
content=content, content=content,
config=config,
registered_oauth=oauth_check, registered_oauth=oauth_check,
mail_configured=config.get_mail_server_configured(), mail_configured=config.get_mail_server_configured(),
kobo_support=kobo_support, kobo_support=kobo_support,
@@ -1591,7 +1635,7 @@ def edit_user(user_id):
page="edituser") page="edituser")
@admi.route("/admin/resetpassword/<int:user_id>") @admi.route("/admin/resetpassword/<int:user_id>", methods=["POST"])
@login_required @login_required
@admin_required @admin_required
def reset_user_password(user_id): def reset_user_password(user_id):
@@ -1767,7 +1811,7 @@ def ldap_import_create_user(user, user_data):
return 0, message return 0, message
@admi.route('/import_ldap_users') @admi.route('/import_ldap_users', methods=["POST"])
@login_required @login_required
@admin_required @admin_required
def import_ldap_users(): def import_ldap_users():
+3
View File
@@ -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('-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('-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('-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() args = parser.parse_args()
settingspath = args.p or os.path.join(_CONFIG_DIR, "app.db") 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 == "": if args.k == "":
keyfilepath = "" keyfilepath = ""
# load covers from localhost
allow_localhost = args.l or None
# handle and check ip address argument # handle and check ip address argument
ip_address = args.i or None ip_address = args.i or None
if ip_address: if ip_address:
+10 -10
View File
@@ -56,25 +56,25 @@ COVER_EXTENSIONS = ['.png', '.webp', '.bmp', '.jpg', '.jpeg']
def _cover_processing(tmp_file_name, img, extension): def _cover_processing(tmp_file_name, img, extension):
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_name), 'cover.jpg') 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: if extension in NO_JPEG_EXTENSIONS:
with Image(filename=tmp_file_name) as imgc: if use_IM:
with Image(blob=img) as imgc:
imgc.format = 'jpeg' imgc.format = 'jpeg'
imgc.transform_colorspace('rgb') imgc.transform_colorspace('rgb')
imgc.save(tmp_cover_name) imgc.save(filename=tmp_cover_name)
return tmp_cover_name return tmp_cover_name
else:
if not img:
return None return None
if img:
with open(tmp_cover_name, 'wb') as f: with open(tmp_cover_name, 'wb') as f:
f.write(img) f.write(img)
return tmp_cover_name return tmp_cover_name
else:
return None
def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecutable): 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': if original_file_extension.upper() == '.CBZ':
cf = zipfile.ZipFile(tmp_file_name) cf = zipfile.ZipFile(tmp_file_name)
for name in cf.namelist(): for name in cf.namelist():
@@ -106,7 +106,7 @@ def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecu
break break
except Exception as ex: except Exception as ex:
log.debug('Rarfile failed with error: %s', 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): 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) cover_data = archive.getPage(index)
break break
else: 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) return _cover_processing(tmp_file_name, cover_data, extension)
+1 -1
View File
@@ -151,7 +151,7 @@ def selected_roles(dictionary):
BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, ' BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, description, tags, series, '
'series_id, languages, publisher') 'series_id, languages, publisher')
STABLE_VERSION = {'version': '0.6.14'} STABLE_VERSION = {'version': '0.6.16'}
NIGHTLY_VERSION = {} NIGHTLY_VERSION = {}
NIGHTLY_VERSION[0] = '$Format:%H$' NIGHTLY_VERSION[0] = '$Format:%H$'
+23 -21
View File
@@ -23,6 +23,7 @@ import re
import ast import ast
import json import json
from datetime import datetime from datetime import datetime
from urllib.parse import quote
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey, CheckConstraint 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) return u"https://portal.issn.org/resource/ISSN/{0}".format(self.val)
elif format_type == "isfdb": elif format_type == "isfdb":
return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val) return u"http://www.isfdb.org/cgi-bin/pl.cgi?{0}".format(self.val)
elif self.val.lower().startswith("javascript:"):
return quote(self.val)
else: else:
return u"{0}".format(self.val) return u"{0}".format(self.val)
@@ -174,8 +177,8 @@ class Comments(Base):
__tablename__ = 'comments' __tablename__ = 'comments'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
book = Column(Integer, ForeignKey('books.id'), nullable=False, unique=True)
text = Column(String(collation='NOCASE'), nullable=False) text = Column(String(collation='NOCASE'), nullable=False)
book = Column(Integer, ForeignKey('books.id'), nullable=False)
def __init__(self, text, book): def __init__(self, text, book):
self.text = text self.text = text
@@ -550,11 +553,8 @@ class CalibreDB():
@classmethod @classmethod
def setup_db(cls, config_calibre_dir, app_db_path): def setup_db(cls, config_calibre_dir, app_db_path):
# cls.config = config
cls.dispose() cls.dispose()
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
if not config_calibre_dir: if not config_calibre_dir:
cls.config.invalidate() cls.config.invalidate()
return False return False
@@ -612,7 +612,7 @@ class CalibreDB():
return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first() return self.session.query(Data).filter(Data.book == book_id).filter(Data.format == file_format).first()
# Language and content filters for displaying in the UI # Language and content filters for displaying in the UI
def common_filters(self, allow_show_archived=False): def common_filters(self, allow_show_archived=False, return_all_languages=False):
if not allow_show_archived: if not allow_show_archived:
archived_books = ( archived_books = (
ub.session.query(ub.ArchivedBook) ub.session.query(ub.ArchivedBook)
@@ -625,10 +625,10 @@ class CalibreDB():
else: else:
archived_filter = true() archived_filter = true()
if current_user.filter_language() != "all": if current_user.filter_language() == "all" or return_all_languages:
lang_filter = Books.languages.any(Languages.lang_code == current_user.filter_language())
else:
lang_filter = true() lang_filter = true()
else:
lang_filter = Books.languages.any(Languages.lang_code == current_user.filter_language())
negtags_list = current_user.list_denied_tags() negtags_list = current_user.list_denied_tags()
postags_list = current_user.list_allowed_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)) 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 # 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): 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 pagination = None
result = self.search_query(term, *join).order_by(*order).all() result = self.search_query(term, *join).order_by(*order).all()
result_count = len(result) result_count = len(result)
@@ -796,27 +796,29 @@ class CalibreDB():
return result[offset:limit_all], result_count, pagination return result[offset:limit_all], result_count, pagination
# Creates for all stored languages a translated speaking name in the array for the UI # 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 from . import get_locale
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: if not languages:
languages = self.session.query(Languages) \ languages = self.session.query(Languages) \
.join(books_languages_link) \ .join(books_languages_link) \
.join(Books) \ .join(Books) \
.filter(self.common_filters()) \ .filter(self.common_filters(return_all_languages=return_all_languages)) \
.group_by(text('books_languages_link.lang_code')).all() .group_by(text('books_languages_link.lang_code')).all()
for lang in languages: for lang in languages:
lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code) lang.name = isoLanguages.get_language_name(get_locale(), lang.lang_code)
#try: return sorted(languages, key=lambda x: x.name, reverse=reverse_order)
# 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
def update_title_sort(self, config, conn=None): def update_title_sort(self, config, conn=None):
# user defined sort function for calibre databases (Series, etc.) # user defined sort function for calibre databases (Series, etc.)
+96
View File
@@ -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
+35 -22
View File
@@ -26,12 +26,13 @@ import json
from shutil import copyfile from shutil import copyfile
from uuid import uuid4 from uuid import uuid4
from markupsafe import escape from markupsafe import escape
from functools import wraps
try: try:
from lxml.html.clean import clean_html from lxml.html.clean import clean_html
except ImportError: except ImportError:
pass pass
# Improve this to check if scholarly is available in a global way, like other pythonic libraries # Improve this to check if scholarly is available in a global way, like other pythonic libraries
try: try:
from scholarly import scholarly from scholarly import scholarly
@@ -52,13 +53,6 @@ from .tasks.upload import TaskUpload
from .render_template import render_title_template from .render_template import render_title_template
from .usermanagement import login_required_if_no_ano 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__) editbook = Blueprint('editbook', __name__)
log = logger.create() log = logger.create()
@@ -67,7 +61,7 @@ log = logger.create()
def upload_required(f): def upload_required(f):
@wraps(f) @wraps(f)
def inner(*args, **kwargs): def inner(*args, **kwargs):
if current_user.role_upload() or current_user.role_admin(): if current_user.role_upload():
return f(*args, **kwargs) return f(*args, **kwargs)
abort(403) abort(403)
@@ -238,17 +232,17 @@ def modify_identifiers(input_identifiers, db_identifiers, db_session):
changed = True changed = True
return changed, error return changed, error
@editbook.route("/ajax/delete/<int:book_id>") @editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@login_required @login_required
def delete_book_from_details(book_id): def delete_book_from_details(book_id):
return Response(delete_book(book_id, "", True), mimetype='application/json') return Response(delete_book_from_table(book_id, "", True), mimetype='application/json')
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}) @editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
@editbook.route("/delete/<int:book_id>/<string:book_format>") @editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
@login_required @login_required
def delete_book_ajax(book_id, book_format): def delete_book_ajax(book_id, book_format):
return delete_book(book_id, book_format, False) return delete_book_from_table(book_id, book_format, False)
def delete_whole_book(book_id, book): 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')) 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 = {} warning = {}
if current_user.role_delete_books(): if current_user.role_delete_books():
book = calibre_db.get_book(book_id) book = calibre_db.get_book(book_id)
@@ -347,6 +341,8 @@ def delete_book(book_id, book_format, jsonResponse):
else: else:
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\ calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
filter(db.Data.format == book_format).delete() 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() calibre_db.session.commit()
except Exception as ex: except Exception as ex:
log.debug_or_exception(ex) log.debug_or_exception(ex)
@@ -364,6 +360,15 @@ def delete_book(book_id, book_format, jsonResponse):
# book not found # book not found
log.error('Book with id "%s" could not be deleted: not found', book_id) 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): 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(): if not series_index.replace('.', '', 1).isdigit():
flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning") flash(_("%(seriesindex)s is not a valid number, skipping", seriesindex=series_index), category="warning")
return False return False
if book.series_index != series_index: if str(book.series_index) != series_index:
book.series_index = series_index book.series_index = series_index
modif_date = True modif_date = True
return modif_date return modif_date
@@ -484,11 +489,11 @@ def edit_book_languages(languages, book, upload=False, invalid=None):
else: else:
input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages) input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages)
for l in unknown_languages: for l in unknown_languages:
log.error('%s is not a valid language', l) log.error("'%s' is not a valid language", l)
if isinstance(invalid, list): if isinstance(invalid, list):
invalid.append(l) invalid.append(l)
else: 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 # ToDo: Not working correct
if upload and len(input_l) == 1: 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 # If the language of the file is excluded from the users view, it's not imported, to allow the user to view
@@ -833,7 +838,7 @@ def edit_book(book_id):
if modif_date: if modif_date:
book.last_modified = datetime.utcnow() 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.merge(book)
calibre_db.session.commit() calibre_db.session.commit()
@@ -848,6 +853,10 @@ def edit_book(book_id):
calibre_db.session.rollback() calibre_db.session.rollback()
flash(error, category="error") flash(error, category="error")
return render_edit_book(book_id) return render_edit_book(book_id)
except 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: except Exception as ex:
log.debug_or_exception(ex) log.debug_or_exception(ex)
calibre_db.session.rollback() 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) modif_date |= edit_book_series_index(meta.series_id, db_book)
# add languages # 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 # handle tags
modif_date |= edit_book_tags(meta.tags, db_book) modif_date |= edit_book_tags(meta.tags, db_book)
@@ -1007,7 +1020,7 @@ def move_coverfile(meta, db_book):
category="error") category="error")
@editbook.route("/upload", methods=["GET", "POST"]) @editbook.route("/upload", methods=["POST"])
@login_required_if_no_ano @login_required_if_no_ano
@upload_required @upload_required
def upload(): def upload():
@@ -1247,7 +1260,7 @@ def merge_list_book():
element.format, element.format,
element.uncompressed_size, element.uncompressed_size,
to_name)) to_name))
delete_book(from_book.id,"", True) delete_book_from_table(from_book.id,"", True)
return json.dumps({'success': True}) return json.dumps({'success': True})
return "" return ""
+1 -1
View File
@@ -109,7 +109,7 @@ def revoke_watch_gdrive():
try: try:
gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'], gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'],
last_watch_response['resourceId']) last_watch_response['resourceId'])
except HttpError: except (HttpError, AttributeError):
pass pass
config.config_google_drive_watch_changes_response = {} config.config_google_drive_watch_changes_response = {}
config.save() config.save()
+21
View File
@@ -35,6 +35,15 @@ except ImportError:
from sqlalchemy.exc import OperationalError, InvalidRequestError from sqlalchemy.exc import OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import text 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: try:
from apiclient import errors from apiclient import errors
from httplib2 import ServerNotFoundError from httplib2 import ServerNotFoundError
@@ -47,11 +56,13 @@ try:
from pydrive2.auth import GoogleAuth from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive from pydrive2.drive import GoogleDrive
from pydrive2.auth import RefreshError from pydrive2.auth import RefreshError
from pydrive2.files import ApiRequestError
except ImportError as err: except ImportError as err:
try: try:
from pydrive.auth import GoogleAuth from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive from pydrive.drive import GoogleDrive
from pydrive.auth import RefreshError from pydrive.auth import RefreshError
from pydrive.files import ApiRequestError
except ImportError as err: except ImportError as err:
importError = err importError = err
gdrive_support = False gdrive_support = False
@@ -313,6 +324,11 @@ def getFolderId(path, drive):
log.error("gdrive.db DB is not Writeable") log.error("gdrive.db DB is not Writeable")
log.debug('Database error: %s', ex) log.debug('Database error: %s', ex)
session.rollback() session.rollback()
except ApiRequestError as ex:
log.error('{} {}'.format(ex.error['message'], path))
session.rollback()
except RefreshError as ex:
log.error(ex)
return currentFolderId return currentFolderId
@@ -659,3 +675,8 @@ def get_error_text(client_secrets=None):
return 'Callback url (redirect url) is missing in client_secrets.json' return 'Callback url (redirect url) is missing in client_secrets.json'
if client_secrets: if client_secrets:
client_secrets.update(filedata['web']) client_secrets.update(filedata['web'])
def get_versions():
return {'six': six_version,
'httplib2': httplib2_version}
+22 -15
View File
@@ -17,18 +17,18 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os import os
import io import io
import mimetypes import mimetypes
import re import re
import shutil import shutil
import time import socket
import unicodedata import unicodedata
from datetime import datetime, timedelta from datetime import datetime, timedelta
from tempfile import gettempdir from tempfile import gettempdir
from urllib.parse import urlparse
import requests import requests
from babel.dates import format_datetime from babel.dates import format_datetime
from babel.units import format_unit from babel.units import format_unit
from flask import send_from_directory, make_response, redirect, abort, url_for from flask import send_from_directory, make_response, redirect, abort, url_for
@@ -38,11 +38,7 @@ from sqlalchemy.sql.expression import true, false, and_, text, func
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from markupsafe import escape from markupsafe import escape
from urllib.parse import quote
try:
from urllib.parse import quote
except ImportError:
from urllib import quote
try: try:
import unidecode import unidecode
@@ -50,7 +46,7 @@ try:
except ImportError: except ImportError:
use_unidecode = False use_unidecode = False
from . import calibre_db from . import calibre_db, cli
from .tasks.convert import TaskConvert from .tasks.convert import TaskConvert
from . import logger, config, get_locale, db, ub from . import logger, config, get_locale, db, ub
from . import gdriveutils as gd from . import gdriveutils as gd
@@ -63,7 +59,7 @@ log = logger.create()
try: try:
from wand.image import Image from wand.image import Image
from wand.exceptions import MissingDelegateError from wand.exceptions import MissingDelegateError, BlobError
use_IM = True use_IM = True
except (ImportError, RuntimeError) as e: except (ImportError, RuntimeError) as e:
log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e) log.debug('Cannot import Image, generating covers from non jpg files will not work: %s', e)
@@ -233,7 +229,7 @@ def get_valid_filename(value, replace_whitespace=True):
value = value[:-1]+u'_' value = value[:-1]+u'_'
value = value.replace("/", "_").replace(":", "_").strip('\0') value = value.replace("/", "_").replace(":", "_").strip('\0')
if use_unidecode: if use_unidecode:
if not config.config_unicode_filename: if config.config_unicode_filename:
value = (unidecode.unidecode(value)) value = (unidecode.unidecode(value))
else: else:
value = value.replace(u'§', u'SS') 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 # saves book cover from url
def save_cover_from_url(url, book_path): def save_cover_from_url(url, book_path):
try: 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 = requests.get(url, timeout=(10, 200)) # ToDo: Error Handling
img.raise_for_status() img.raise_for_status()
return save_cover(img, book_path) return save_cover(img, book_path)
except (requests.exceptions.HTTPError, except (socket.gaierror,
requests.exceptions.HTTPError,
requests.exceptions.ConnectionError, requests.exceptions.ConnectionError,
requests.exceptions.Timeout) as ex: requests.exceptions.Timeout) as ex:
log.info(u'Cover Download Error %s', ex) log.info(u'Cover Download Error %s', ex)
@@ -638,6 +641,7 @@ def save_cover(img, book_path):
return False, _("Only jpg/jpeg/png/webp/bmp files are supported as coverfile") return False, _("Only jpg/jpeg/png/webp/bmp files are supported as coverfile")
# convert to jpg because calibre only supports jpg # convert to jpg because calibre only supports jpg
if content_type != 'image/jpg': if content_type != 'image/jpg':
try:
if hasattr(img, 'stream'): if hasattr(img, 'stream'):
imgc = Image(blob=img.stream) imgc = Image(blob=img.stream)
else: else:
@@ -645,6 +649,9 @@ def save_cover(img, book_path):
imgc.format = 'jpeg' imgc.format = 'jpeg'
imgc.transform_colorspace("rgb") imgc.transform_colorspace("rgb")
img = imgc img = imgc
except (BlobError, MissingDelegateError):
log.error("Invalid cover file content")
return False, _("Invalid cover file content")
else: else:
if content_type not in 'image/jpeg': if content_type not in 'image/jpeg':
log.error("Only jpg/jpeg files are supported as coverfile") 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): def do_download_file(book, book_format, client, data, headers):
if config.config_use_google_drive: if config.config_use_google_drive:
startTime = time.time() #startTime = time.time()
df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format) df = gd.getFileFromEbooksFolder(book.path, data.name + "." + book_format)
log.debug('%s', time.time() - startTime) #log.debug('%s', time.time() - startTime)
if df: if df:
return gd.do_gdrive_download(df, headers) return gd.do_gdrive_download(df, headers)
else: else:
@@ -819,7 +826,7 @@ def get_cc_columns(filter_config_custom_read=False):
def get_download_link(book_id, book_format, client): def get_download_link(book_id, book_format, client):
book_format = book_format.split(".")[0] 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: if book:
data1 = calibre_db.get_book_format(book.id, book_format.upper()) data1 = calibre_db.get_book_format(book.id, book_format.upper())
else: else:
+7
View File
@@ -17,6 +17,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES from .iso_language_names import LANGUAGE_NAMES as _LANGUAGE_NAMES
from . import logger
log = logger.create()
try: try:
@@ -50,7 +53,11 @@ def get_language_names(locale):
def get_language_name(locale, lang_code): def get_language_name(locale, lang_code):
try:
return get_language_names(locale)[lang_code] 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): def get_language_codes(locale, language_names, remainder=None):
File diff suppressed because it is too large Load Diff
+73 -43
View File
@@ -22,11 +22,8 @@ import datetime
import os import os
import uuid import uuid
from time import gmtime, strftime from time import gmtime, strftime
import json
try: from urllib.parse import unquote
from urllib import unquote
except ImportError:
from urllib.parse import unquote
from flask import ( from flask import (
Blueprint, Blueprint,
@@ -102,6 +99,8 @@ def make_request_to_kobo_store(sync_token=None):
allow_redirects=False, allow_redirects=False,
timeout=(2, 10) timeout=(2, 10)
) )
log.debug("Content: " + str(store_response.content))
log.debug("StatusCode: " + str(store_response.status_code))
return store_response return store_response
@@ -110,7 +109,8 @@ def redirect_or_proxy_request():
if request.method == "GET": if request.method == "GET":
return redirect(get_store_url_for_current_request(), 307) return redirect(get_store_url_for_current_request(), 307)
else: 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() store_response = make_request_to_kobo_store()
response_headers = store_response.headers response_headers = store_response.headers
@@ -142,12 +142,16 @@ def HandleSyncRequest():
if not current_app.wsgi_app.is_proxied: if not current_app.wsgi_app.is_proxied:
log.debug('Kobo: Received unproxied request, changed request port to external server port') log.debug('Kobo: Received unproxied request, changed request port to external server port')
# TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header # if no books synced don't respect sync_token
# instead so that the device triggers another sync. 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_modified = sync_token.books_last_modified # needed for sync selected shelfs only
new_books_last_created = sync_token.books_last_created 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_reading_state_last_modified = sync_token.reading_state_last_modified
new_archived_last_modified = datetime.datetime.min new_archived_last_modified = datetime.datetime.min
sync_results = [] sync_results = []
@@ -169,10 +173,10 @@ def HandleSyncRequest():
ub.BookShelf.date_added, ub.BookShelf.date_added,
ub.ArchivedBook.is_archived) ub.ArchivedBook.is_archived)
changed_entries = (changed_entries changed_entries = (changed_entries
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) .join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True) ub.ArchivedBook.user_id == current_user.id))
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id, .filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
ub.KoboSyncedBooks.book_id == None)) .filter(ub.KoboSyncedBooks.user_id == current_user.id)))
.filter(ub.BookShelf.date_added > sync_token.books_last_modified) .filter(ub.BookShelf.date_added > sync_token.books_last_modified)
.filter(db.Data.format.in_(KOBO_FORMATS)) .filter(db.Data.format.in_(KOBO_FORMATS))
.filter(calibre_db.common_filters(allow_show_archived=True)) .filter(calibre_db.common_filters(allow_show_archived=True))
@@ -192,11 +196,11 @@ def HandleSyncRequest():
ub.ArchivedBook.last_modified, ub.ArchivedBook.last_modified,
ub.ArchivedBook.is_archived) ub.ArchivedBook.is_archived)
changed_entries = (changed_entries changed_entries = (changed_entries
.join(db.Data).outerjoin(ub.ArchivedBook, db.Books.id == ub.ArchivedBook.book_id) .join(db.Data).outerjoin(ub.ArchivedBook, and_(db.Books.id == ub.ArchivedBook.book_id,
.join(ub.KoboSyncedBooks, ub.KoboSyncedBooks.book_id == db.Books.id, isouter=True) ub.ArchivedBook.user_id == current_user.id))
.filter(or_(ub.KoboSyncedBooks.user_id != current_user.id, .filter(db.Books.id.notin_(calibre_db.session.query(ub.KoboSyncedBooks.book_id)
ub.KoboSyncedBooks.book_id == None)) .filter(ub.KoboSyncedBooks.user_id == current_user.id)))
.filter(calibre_db.common_filters()) .filter(calibre_db.common_filters(allow_show_archived=True))
.filter(db.Data.format.in_(KOBO_FORMATS)) .filter(db.Data.format.in_(KOBO_FORMATS))
.order_by(db.Books.last_modified) .order_by(db.Books.last_modified)
.order_by(db.Books.id) .order_by(db.Books.id)
@@ -208,8 +212,8 @@ def HandleSyncRequest():
books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT)) books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT))
else: else:
books = changed_entries.limit(SYNC_ITEM_LIMIT) books = changed_entries.limit(SYNC_ITEM_LIMIT)
log.debug("Books to Sync: {}".format(len(books.all())))
for book in books: for book in books:
kobo_sync_status.add_synced_books(book.Books.id)
formats = [data.format for data in book.Books.data] formats = [data.format for data in book.Books.data]
if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: 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) helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name)
@@ -248,14 +252,17 @@ def HandleSyncRequest():
pass pass
new_books_last_created = max(ts_created, new_books_last_created) new_books_last_created = max(ts_created, new_books_last_created)
kobo_sync_status.add_synced_books(book.Books.id)
if sqlalchemy_version2: if sqlalchemy_version2:
max_change = calibre_db.session.execute(changed_entries max_change = calibre_db.session.execute(changed_entries
.filter(ub.ArchivedBook.is_archived) .filter(ub.ArchivedBook.is_archived)
.filter(ub.ArchivedBook.user_id == current_user.id)
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\ .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\
.columns(db.Books).first() .columns(db.Books).first()
else: 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() .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first()
max_change = max_change.last_modified if max_change else new_archived_last_modified 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( changed_reading_states = changed_reading_states.filter(
and_(ub.KoboReadingState.user_id == current_user.id, 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) cont_sync |= bool(changed_reading_states.count() > SYNC_ITEM_LIMIT)
for kobo_reading_state in changed_reading_states.limit(SYNC_ITEM_LIMIT).all(): 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() 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_shelves(sync_token, sync_results, only_kobo_shelves)
# 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_created = new_books_last_created
sync_token.books_last_modified = new_books_last_modified sync_token.books_last_modified = new_books_last_modified
sync_token.archive_last_modified = new_archived_last_modified sync_token.archive_last_modified = new_archived_last_modified
sync_token.reading_state_last_modified = new_reading_state_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) return generate_sync_response(sync_token, sync_results, cont_sync)
def generate_sync_response(sync_token, sync_results, set_cont=False): def generate_sync_response(sync_token, sync_results, set_cont=False):
extra_headers = {} 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. # Merge in sync results from the official Kobo store.
try: try:
store_response = make_request_to_kobo_store(sync_token) 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) sync_token.to_headers(extra_headers)
# log.debug("Kobo Sync Content: {}".format(sync_results)) # 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 return response
@@ -352,7 +362,9 @@ def HandleMetadataRequest(book_uuid):
return redirect_or_proxy_request() return redirect_or_proxy_request()
metadata = get_metadata(book) 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): 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): def create_book_entitlement(book, archived):
book_uuid = book.uuid book_uuid = str(book.uuid)
return { return {
"Accessibility": "Full", "Accessibility": "Full",
"ActivePeriod": {"From": convert_to_kobo_timestamp_string(datetime.datetime.now())}, "ActivePeriod": {"From": convert_to_kobo_timestamp_string(datetime.datetime.now())},
@@ -407,18 +419,15 @@ def get_description(book):
return book.comments[0].text return book.comments[0].text
# TODO handle multiple authors
def get_author(book): def get_author(book):
if not book.authors: if not book.authors:
return {"Contributors": None} return {"Contributors": None}
if len(book.authors) > 1:
author_list = [] author_list = []
autor_roles = [] autor_roles = []
for author in book.authors: for author in book.authors:
autor_roles.append({"Name":author.name, "Role":"Author"}) autor_roles.append({"Name":author.name}) #.encode('unicode-escape').decode('latin-1')
author_list.append(author.name) author_list.append(author.name)
return {"ContributorRoles": autor_roles, "Contributors":author_list} return {"ContributorRoles": autor_roles, "Contributors":author_list}
return {"ContributorRoles": [{"Name":book.authors[0].name, "Role":"Author"}], "Contributors": book.authors[0].name}
def get_publisher(book): def get_publisher(book):
@@ -475,9 +484,7 @@ def get_metadata(book):
"IsSocialEnabled": True, "IsSocialEnabled": True,
"Language": "en", "Language": "en",
"PhoneticPronunciations": {}, "PhoneticPronunciations": {},
# TODO: Fix book.pubdate to return a datetime object so that we can easily "PublicationDate": convert_to_kobo_timestamp_string(book.pubdate),
# convert it to the format Kobo devices expect.
"PublicationDate": book.pubdate,
"Publisher": {"Imprint": "", "Name": get_publisher(book),}, "Publisher": {"Imprint": "", "Name": get_publisher(book),},
"RevisionId": book_uuid, "RevisionId": book_uuid,
"Title": book.title, "Title": book.title,
@@ -492,7 +499,7 @@ def get_metadata(book):
"Number": get_seriesindex(book), # ToDo Check int() ? "Number": get_seriesindex(book), # ToDo Check int() ?
"NumberFloat": float(get_seriesindex(book)), "NumberFloat": float(get_seriesindex(book)),
# Get a deterministic id based on the series name. # 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 return metadata
@@ -667,11 +674,8 @@ def HandleTagRemoveItem(tag_id):
# Note: Public shelves that aren't owned by the user aren't supported. # Note: Public shelves that aren't owned by the user aren't supported.
def sync_shelves(sync_token, sync_results, only_kobo_shelves=False): def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
new_tags_last_modified = sync_token.tags_last_modified new_tags_last_modified = sync_token.tags_last_modified
# transmit all archived shelfs independent of last sync (why should this matter?)
for shelf in ub.session.query(ub.ShelfArchive).filter( for shelf in ub.session.query(ub.ShelfArchive).filter(ub.ShelfArchive.user_id == current_user.id):
func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified,
ub.ShelfArchive.user_id == current_user.id
):
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified) new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
sync_results.append({ sync_results.append({
"DeletedTag": { "DeletedTag": {
@@ -684,7 +688,6 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False):
ub.session.delete(shelf) ub.session.delete(shelf)
ub.session_commit() ub.session_commit()
extra_filters = [] extra_filters = []
if only_kobo_shelves: if only_kobo_shelves:
for shelf in ub.session.query(ub.Shelf).filter( 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): def get_or_create_reading_state(book_id):
book_read = ub.session.query(ub.ReadBook).filter(ub.ReadBook.book_id == 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: if not book_read:
book_read = ub.ReadBook(user_id=current_user.id, book_id=book_id) book_read = ub.ReadBook(user_id=current_user.id, book_id=book_id)
if not book_read.kobo_reading_state: if not book_read.kobo_reading_state:
@@ -961,6 +964,8 @@ def HandleBookDeletionRequest(book_uuid):
ub.session.merge(archived_book) ub.session.merge(archived_book)
ub.session_commit() ub.session_commit()
if archived_book.is_archived:
kobo_sync_status.remove_synced_book(book_id)
return "", 204 return "", 204
@@ -984,16 +989,41 @@ def HandleUserRequest(dummy=None):
return redirect_or_proxy_request() 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 @csrf.exempt
@kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"]) @kobo.route("/v1/products/<dummy>/prices", methods=["GET", "POST"])
@kobo.route("/v1/products/<dummy>/recommendations", 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>/nextread", methods=["GET", "POST"])
@kobo.route("/v1/products/<dummy>/reviews", 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/external/<dummy>", methods=["GET", "POST"])
@kobo.route("/v1/products/books/series/<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/books/<dummy>/", methods=["GET", "POST"])
@kobo.route("/v1/products/dailydeal", 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/products", methods=["GET", "POST"])
@kobo.route("/v1/affiliate", methods=["GET", "POST"])
@kobo.route("/v1/deals", methods=["GET", "POST"])
def HandleProductsRequest(dummy=None): def HandleProductsRequest(dummy=None):
log.debug("Unimplemented Products Request received: %s", request.base_url) log.debug("Unimplemented Products Request received: %s", request.base_url)
return redirect_or_proxy_request() return redirect_or_proxy_request()
+10 -20
View File
@@ -62,6 +62,7 @@ particular calls to non-Kobo specific endpoints such as the CalibreWeb book down
from binascii import hexlify from binascii import hexlify
from datetime import datetime from datetime import datetime
from os import urandom from os import urandom
from functools import wraps
from flask import g, Blueprint, url_for, abort, request from flask import g, Blueprint, url_for, abort, request
from flask_login import login_user, current_user, login_required 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 . import logger, config, calibre_db, db, helper, ub, lm
from .render_template import render_title_template from .render_template import render_title_template
try:
from functools import wraps
except ImportError:
pass # We're not using Python 3
log = logger.create() log = logger.create()
@@ -122,20 +118,16 @@ kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth")
@kobo_auth.route("/generate_auth_token/<int:user_id>") @kobo_auth.route("/generate_auth_token/<int:user_id>")
@login_required @login_required
def generate_auth_token(user_id): def generate_auth_token(user_id):
warning = False
host_list = request.host.rsplit(':') host_list = request.host.rsplit(':')
if len(host_list) == 1: if len(host_list) == 1:
host = ':'.join(host_list) host = ':'.join(host_list)
else: else:
host = ':'.join(host_list[0:-1]) host = ':'.join(host_list[0:-1])
if host.startswith('127.') or host.lower() == 'localhost' or host.startswith('[::ffff:7f'): 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') 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", # Generate auth token if none is existing for this user
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( auth_token = ub.session.query(ub.RemoteAuthToken).filter(
ub.RemoteAuthToken.user_id == user_id ub.RemoteAuthToken.user_id == user_id
).filter(ub.RemoteAuthToken.token_type==1).first() ).filter(ub.RemoteAuthToken.token_type==1).first()
@@ -160,17 +152,15 @@ def generate_auth_token(user_id):
return render_title_template( return render_title_template(
"generate_kobo_auth_url.html", "generate_kobo_auth_url.html",
title=_(u"Kobo Setup"), title=_(u"Kobo Setup"),
kobo_auth_url=url_for( auth_token=auth_token.auth_token,
"kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True warning = warning
),
warning = False
) )
@kobo_auth.route("/deleteauthtoken/<int:user_id>") @kobo_auth.route("/deleteauthtoken/<int:user_id>", methods=["POST"])
@login_required @login_required
def delete_auth_token(user_id): 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)\ ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\
.filter(ub.RemoteAuthToken.token_type==1).delete() .filter(ub.RemoteAuthToken.token_type==1).delete()
+40 -23
View File
@@ -20,10 +20,15 @@
from flask_login import current_user from flask_login import current_user
from . import ub from . import ub
import datetime 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): def add_synced_books(book_id):
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 = ub.KoboSyncedBooks()
synced_book.user_id = current_user.id synced_book.user_id = current_user.id
synced_book.book_id = book_id synced_book.book_id = book_id
@@ -31,41 +36,53 @@ def add_synced_books(book_id):
ub.session_commit() ub.session_commit()
def remove_synced_book(book_id): # Select all entries of current book in kobo_synced_books table, which are from current user and delete them
ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.book_id == book_id).delete() 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() 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) def change_archived_books(book_id, state=None, message=None):
.filter(ub.ArchivedBook.book_id == book_id) archived_book = ub.session.query(ub.ArchivedBook).filter(and_(ub.ArchivedBook.user_id == int(current_user.id),
.first() ub.ArchivedBook.book_id == book_id)).first()
)
if not archived_book: if not archived_book:
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id) 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() archived_book.last_modified = datetime.datetime.utcnow()
ub.session.merge(archived_book) 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 books which are synced by the current user and do not belong to a synced shelf and them to archive # select all shelves from current user which are synced and do not belong to the "only sync" shelves
# select all shelfs from current user which are synced and do not belong to the "only sync" shelfs def update_on_sync_shelfs(user_id):
def update_on_sync_shelfs(content_id):
books_to_archive = (ub.session.query(ub.KoboSyncedBooks) books_to_archive = (ub.session.query(ub.KoboSyncedBooks)
.join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True) .join(ub.BookShelf, ub.KoboSyncedBooks.book_id == ub.BookShelf.book_id, isouter=True)
.join(ub.Shelf, ub.Shelf.user_id == content_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(or_(ub.Shelf.kobo_sync == 0, ub.Shelf.kobo_sync == None))
.filter(ub.KoboSyncedBooks.user_id == content_id).all()) .filter(ub.KoboSyncedBooks.user_id == user_id).all())
for b in books_to_archive: for b in books_to_archive:
add_archived_books(b.book_id) 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 == content_id).delete() ub.session.query(ub.KoboSyncedBooks) \
.filter(ub.KoboSyncedBooks.book_id == b.book_id) \
.filter(ub.KoboSyncedBooks.user_id == user_id).delete()
ub.session_commit() ub.session_commit()
shelfs_to_archive = ub.session.query(ub.Shelf).filter(ub.Shelf.user_id == content_id).filter( # 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() ub.Shelf.kobo_sync == 0).all()
for a in shelfs_to_archive: for a in shelves_to_archive:
ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=content_id)) ub.session.add(ub.ShelfArchive(uuid=a.uuid, user_id=user_id))
ub.session_commit() ub.session_commit()
+4 -9
View File
@@ -42,22 +42,17 @@ logging.addLevelName(logging.CRITICAL, "CRIT")
class _Logger(logging.Logger): 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 sys.version_info > (3, 7):
if is_debug_enabled(): if is_debug_enabled():
self.exception(message, stacklevel=2, *args, **kwargs) self.exception(message, stacklevel=stacklevel, *args, **kwargs)
else:
self.error(message, stacklevel=stacklevel, *args, **kwargs)
else: else:
self.error(message, stacklevel=2, *args, **kwargs)
elif sys.version_info > (3, 0):
if is_debug_enabled(): if is_debug_enabled():
self.exception(message, stack_info=True, *args, **kwargs) self.exception(message, stack_info=True, *args, **kwargs)
else: else:
self.error(message, *args, **kwargs) self.error(message, *args, **kwargs)
else:
if is_debug_enabled():
self.exception(message, *args, **kwargs)
else:
self.error(message, *args, **kwargs)
def debug_no_auth(self, message, *args, **kwargs): def debug_no_auth(self, message, *args, **kwargs):
+2 -2
View File
@@ -26,7 +26,7 @@ class ComicVine(Metadata):
__name__ = "ComicVine" __name__ = "ComicVine"
__id__ = "comicvine" __id__ = "comicvine"
def search(self, query, __): def search(self, query, generic_cover=""):
val = list() val = list()
apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6" apikey = "57558043c53943d5d1e96a9ad425b0eb85532ee6"
if self.active: if self.active:
@@ -36,7 +36,7 @@ class ComicVine(Metadata):
result = requests.get("https://comicvine.gamespot.com/api/search?api_key=" result = requests.get("https://comicvine.gamespot.com/api/search?api_key="
+ apikey + "&resources=issue&query=" + query + "&sort=name:desc&format=json", headers=headers) + 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', "") seriesTitle = r['volume'].get('name', "")
if r.get('store_date'): if r.get('store_date'):
dateFomers = r.get('store_date') dateFomers = r.get('store_date')
+3 -3
View File
@@ -26,14 +26,14 @@ class Google(Metadata):
__name__ = "Google" __name__ = "Google"
__id__ = "google" __id__ = "google"
def search(self, query, __): def search(self, query, generic_cover=""):
if self.active: if self.active:
val = list() val = list()
result = requests.get("https://www.googleapis.com/books/v1/volumes?q="+query.replace(" ","+")) 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 = dict()
v['id'] = r['id'] v['id'] = r['id']
v['title'] = r['volumeInfo']['title'] v['title'] = r['volumeInfo'].get('title',"")
v['authors'] = r['volumeInfo'].get('authors', []) v['authors'] = r['volumeInfo'].get('authors', [])
v['description'] = r['volumeInfo'].get('description', "") v['description'] = r['volumeInfo'].get('description', "")
v['publisher'] = r['volumeInfo'].get('publisher', "") v['publisher'] = r['volumeInfo'].get('publisher', "")
+4 -5
View File
@@ -20,7 +20,6 @@ from scholarly import scholarly
from cps.services.Metadata import Metadata from cps.services.Metadata import Metadata
class scholar(Metadata): class scholar(Metadata):
__name__ = "Google Scholar" __name__ = "Google Scholar"
__id__ = "googlescholar" __id__ = "googlescholar"
@@ -32,7 +31,7 @@ class scholar(Metadata):
i = 0 i = 0
for publication in scholar_gen: for publication in scholar_gen:
v = dict() v = dict()
v['id'] = "1234" # publication['bib'].get('title') v['id'] = publication['url_scholarbib'].split(':')[1]
v['title'] = publication['bib'].get('title') v['title'] = publication['bib'].get('title')
v['authors'] = publication['bib'].get('author', []) v['authors'] = publication['bib'].get('author', [])
v['description'] = publication['bib'].get('abstract', "") v['description'] = publication['bib'].get('abstract', "")
@@ -41,10 +40,10 @@ class scholar(Metadata):
v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01" v['publishedDate'] = publication['bib'].get('pub_year')+"-01-01"
else: else:
v['publishedDate'] = "" v['publishedDate'] = ""
v['tags'] = "" v['tags'] = []
v['ratings'] = 0 v['rating'] = 0
v['series'] = "" v['series'] = ""
v['cover'] = generic_cover v['cover'] = ""
v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "", v['url'] = publication.get('pub_url') or publication.get('eprint_url') or "",
v['source'] = { v['source'] = {
"id": self.__id__, "id": self.__id__,
-3
View File
@@ -20,7 +20,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import datetime import datetime
from functools import wraps from functools import wraps
@@ -35,8 +34,6 @@ from .pagination import Pagination
from .web import render_read_books from .web import render_read_books
from .usermanagement import load_user_from_request from .usermanagement import load_user_from_request
from flask_babel import gettext as _ from flask_babel import gettext as _
from babel import Locale as LC
from babel.core import UnknownLocaleError
opds = Blueprint('opds', __name__) opds = Blueprint('opds', __name__)
+2 -4
View File
@@ -27,10 +27,8 @@
# http://flask.pocoo.org/snippets/62/ # http://flask.pocoo.org/snippets/62/
try: from urllib.parse import urlparse, urljoin
from urllib.parse import urlparse, urljoin
except ImportError:
from urlparse import urlparse, urljoin
from flask import request, url_for, redirect from flask import request, url_for, redirect
+2 -2
View File
@@ -103,9 +103,9 @@ def metadata_search():
data = list() data = list()
active = current_user.view_settings.get('metadata', {}) active = current_user.view_settings.get('metadata', {})
if query: if query:
static_cover = url_for('static', filename='generic_cover.jpg') generic_cover = ""
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: 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): for future in concurrent.futures.as_completed(meta):
data.extend(future.result()) data.extend(future.result())
return Response(json.dumps(data), mimetype='application/json') return Response(json.dumps(data), mimetype='application/json')
+4 -8
View File
@@ -21,11 +21,8 @@ import sys
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__ from jsonschema import validate, exceptions, __version__
from datetime import datetime from datetime import datetime
try:
# pylint: disable=unused-import from urllib.parse import unquote
from urllib import unquote
except ImportError:
from urllib.parse import unquote
from flask import json from flask import json
from .. import logger from .. import logger
@@ -182,10 +179,9 @@ class SyncToken:
return b64encode_json(token) return b64encode_json(token)
def __str__(self): def __str__(self):
return "{},{},{},{},{},{}".format(self.raw_kobo_store_token, return "{},{},{},{},{},{}".format(self.books_last_created,
self.books_last_created,
self.books_last_modified, self.books_last_modified,
self.archive_last_modified, self.archive_last_modified,
self.reading_state_last_modified, self.reading_state_last_modified,
self.tags_last_modified) self.tags_last_modified, self.raw_kobo_store_token)
#self.books_last_id) #self.books_last_id)
+1 -1
View File
@@ -85,7 +85,7 @@ class WorkerThread(threading.Thread):
def add(cls, user, task): def add(cls, user, task):
ins = cls.getInstance() ins = cls.getInstance()
ins.num += 1 ins.num += 1
log.debug("Add Task for user: {}: {}".format(user, task)) log.debug("Add Task for user: {} - {}".format(user, task))
ins.queue.put(QueuedTask( ins.queue.put(QueuedTask(
num=ins.num, num=ins.num,
user=user, user=user,
+22 -12
View File
@@ -56,7 +56,7 @@ def check_shelf_view_permissions(cur_shelf):
return True 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 @login_required
def add_to_shelf(shelf_id, book_id): def add_to_shelf(shelf_id, book_id):
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest' xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
@@ -112,7 +112,7 @@ def add_to_shelf(shelf_id, book_id):
return "", 204 return "", 204
@shelf.route("/shelf/massadd/<int:shelf_id>") @shelf.route("/shelf/massadd/<int:shelf_id>", methods=["POST"])
@login_required @login_required
def search_to_shelf(shelf_id): def search_to_shelf(shelf_id):
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
@@ -122,8 +122,8 @@ def search_to_shelf(shelf_id):
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if not check_shelf_edit_permissions(shelf): if not check_shelf_edit_permissions(shelf):
log.warning("You are not allowed to add a book to the the shelf: {}".format(shelf.name)) 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 the shelf: %(name)s", name=shelf.name), category="error") flash(_(u"You are not allowed to add a book to the shelf"), category="error")
return redirect(url_for('web.index')) return redirect(url_for('web.index'))
if current_user.id in ub.searched_ids and ub.searched_ids[current_user.id]: 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')) 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 @login_required
def remove_from_shelf(shelf_id, book_id): def remove_from_shelf(shelf_id, book_id):
xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest' xhr = request.headers.get('X-Requested-With') == 'XMLHttpRequest'
@@ -215,10 +215,10 @@ def remove_from_shelf(shelf_id, book_id):
else: else:
if not xhr: if not xhr:
log.warning("You are not allowed to remove a book from shelf: {}".format(shelf.name)) 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") category="error")
return redirect(url_for('web.index')) 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"]) @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") return create_edit_shelf(shelf, page_title=_(u"Create a Shelf"), page="shelfcreate")
@shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"]) @shelf.route("/shelf/edit/<int:shelf_id>", methods=["GET", "POST"])
@login_required @login_required
def edit_shelf(shelf_id): def edit_shelf(shelf_id):
@@ -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() # calibre_db.session.query(ub.Shelf).filter(ub.Shelf.user_id == current_user.id).filter(ub.Shelf.kobo_sync).count()
if request.method == "POST": if request.method == "POST":
to_save = request.form.to_dict() 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: if config.config_kobo_sync:
shelf.kobo_sync = True if to_save.get("kobo_sync") else False shelf.kobo_sync = True if to_save.get("kobo_sync") else False
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", "") 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.name = shelf_title
shelf.is_public = is_public
if not shelf_id: if not shelf_id:
shelf.user_id = int(current_user.id) shelf.user_id = int(current_user.id)
ub.session.add(shelf) ub.session.add(shelf)
@@ -280,12 +289,12 @@ def create_edit_shelf(shelf, page_title, page, shelf_id=False):
sync_only_selected_shelves=sync_only_selected_shelves) 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: if shelf_id:
ident = ub.Shelf.id != shelf_id ident = ub.Shelf.id != shelf_id
else: else:
ident = true() ident = true()
if shelf.is_public == 1: if is_public == 1:
is_shelf_name_unique = ub.session.query(ub.Shelf) \ is_shelf_name_unique = ub.session.query(ub.Shelf) \
.filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \ .filter((ub.Shelf.name == title) & (ub.Shelf.is_public == 1)) \
.filter(ident) \ .filter(ident) \
@@ -319,12 +328,13 @@ def delete_shelf_helper(cur_shelf):
ub.session_commit("successfully deleted Shelf {}".format(cur_shelf.name)) 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 @login_required
def delete_shelf(shelf_id): def delete_shelf(shelf_id):
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first() cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
try: try:
delete_shelf_helper(cur_shelf) delete_shelf_helper(cur_shelf)
flash(_("Shelf successfully deleted"), category="success")
except InvalidRequestError: except InvalidRequestError:
ub.session.rollback() ub.session.rollback()
log.error("Settings DB is not Writeable") log.error("Settings DB is not Writeable")
+14 -7
View File
@@ -169,9 +169,14 @@ table .bg-dark-danger a { color: #fff; }
position: static; position: static;
} }
.container-fluid .book .cover span .img {
bottom: 0;
height: 100%;
position: absolute;
}
.container-fluid .book .cover span img { .container-fluid .book .cover span img {
border: 1px solid #fff; border: 1px solid #fff;
bottom: 0;
position: relative; position: relative;
height: 100%; height: 100%;
@@ -246,8 +251,10 @@ span.glyphicon.glyphicon-tags {
} }
.cover .read { .cover .read {
left: auto; position: relative;
right: 2px; top: -20px;
/*left: auto;
right: 2px;*/
width: 17px; width: 17px;
height: 17px; height: 17px;
display: inline-block; display: inline-block;
@@ -422,7 +429,7 @@ div.log {
} }
#detailcover { cursor:zoom-in; } #detailcover { cursor:zoom-in; }
#detailcover:-webkit-full-screen { cursor:zoom-out; } #detailcover:-webkit-full-screen { cursor:zoom-out; border: 0; }
#detailcover:-moz-full-screen { cursor:zoom-out; } #detailcover:-moz-full-screen { cursor:zoom-out; border: 0; }
#detailcover:-ms-fullscreen { cursor:zoom-out; } #detailcover:-ms-fullscreen { cursor:zoom-out; border: 0; }
#detailcover:fullscreen { cursor:zoom-out; } #detailcover:fullscreen { cursor:zoom-out; border: 0; }
+5 -2
View File
@@ -61,8 +61,11 @@ $("#archived_cb").on("change", function() {
$("#shelf-actions").on("click", "[data-shelf-action]", function (e) { $("#shelf-actions").on("click", "[data-shelf-action]", function (e) {
e.preventDefault(); e.preventDefault();
$.ajax({
$.get(this.href) url: this.href,
method:"post",
data: {csrf_token:$("input[name='csrf_token']").val()},
})
.done(function() { .done(function() {
var $this = $(this); var $this = $(this);
switch ($this.data("shelf-action")) { switch ($this.data("shelf-action")) {
+2 -2
View File
@@ -248,7 +248,7 @@ $("#btn-upload-format").on("change", function () {
if (filename.substring(3, 11) === "fakepath") { if (filename.substring(3, 11) === "fakepath") {
filename = filename.substring(12); filename = filename.substring(12);
} // Remove c:\fake at beginning from localhost chrome } // Remove c:\fake at beginning from localhost chrome
$("#upload-format").html(filename); $("#upload-format").text(filename);
}); });
$("#btn-upload-cover").on("change", function () { $("#btn-upload-cover").on("change", function () {
@@ -256,7 +256,7 @@ $("#btn-upload-cover").on("change", function () {
if (filename.substring(3, 11) === "fakepath") { if (filename.substring(3, 11) === "fakepath") {
filename = filename.substring(12); filename = filename.substring(12);
} // Remove c:\fake at beginning from localhost chrome } // Remove c:\fake at beginning from localhost chrome
$("#upload-cover").html(filename); $("#upload-cover").text(filename);
}); });
$("#xchange").click(function () { $("#xchange").click(function () {
+21 -6
View File
@@ -21,47 +21,59 @@ var $list = $("#list").isotope({
itemSelector: ".book", itemSelector: ".book",
layoutMode: "fitRows", layoutMode: "fitRows",
getSortData: { getSortData: {
title: ".title", title: ".title"
} },
}); });
$("#desc").click(function() { $("#desc").click(function() {
if (direction === 0) { if (direction === 0) {
return; return;
} }
$("#asc").removeClass("active");
$("#desc").addClass("active");
var page = $(this).data("id"); var page = $(this).data("id");
$.ajax({ $.ajax({
method:"post", method:"post",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/view", url: getPath() + "/ajax/view",
data: "{\"" + page + "\": {\"dir\": \"desc\"}}", data: "{\"" + page + "\": {\"dir\": \"desc\"}}",
}); });
// invert sorting order to make already inverted start order working
$list.isotope({ $list.isotope({
sortBy: "name", sortBy: "name",
sortAscending: true sortAscending: !$list.data('isotope').options.sortAscending
}); });
direction = 0;
}); });
$("#asc").click(function() { $("#asc").click(function() {
if (direction === 1) { if (direction === 1) {
return; return;
} }
$("#desc").removeClass("active");
$("#asc").addClass("active");
var page = $(this).data("id"); var page = $(this).data("id");
$.ajax({ $.ajax({
method:"post", method:"post",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/view", url: getPath() + "/ajax/view",
data: "{\"" + page + "\": {\"dir\": \"asc\"}}", data: "{\"" + page + "\": {\"dir\": \"asc\"}}",
}); });
$list.isotope({ $list.isotope({
sortBy: "name", sortBy: "name",
sortAscending: false sortAscending: !$list.data('isotope').options.sortAscending
}); });
direction = 1;
}); });
$("#all").click(function() { $("#all").click(function() {
$(".char").removeClass("active");
$("#all").addClass("active");
// go through all elements and make them visible // go through all elements and make them visible
$list.isotope({ filter: function() { $list.isotope({ filter: function() {
return true; return true;
@@ -70,6 +82,9 @@ $("#all").click(function() {
}); });
$(".char").click(function() { $(".char").click(function() {
$(".char").removeClass("active");
$(this).addClass("active");
$("#all").removeClass("active");
var character = this.innerText; var character = this.innerText;
$list.isotope({ filter: function() { $list.isotope({ filter: function() {
return this.attributes["data-id"].value.charAt(0).toUpperCase() === character; return this.attributes["data-id"].value.charAt(0).toUpperCase() === character;
+14 -3
View File
@@ -19,6 +19,7 @@ var direction = $("#asc").data('order'); // 0=Descending order; 1= ascending or
var sort = 0; // Show sorted entries var sort = 0; // Show sorted entries
$("#sort_name").click(function() { $("#sort_name").click(function() {
$("#sort_name").toggleClass("active");
var className = $("h1").attr("Class") + "_sort_name"; var className = $("h1").attr("Class") + "_sort_name";
var obj = {}; var obj = {};
obj[className] = sort; obj[className] = sort;
@@ -68,12 +69,15 @@ $("#desc").click(function() {
if (direction === 0) { if (direction === 0) {
return; return;
} }
$("#asc").removeClass("active");
$("#desc").addClass("active");
var page = $(this).data("id"); var page = $(this).data("id");
$.ajax({ $.ajax({
method:"post", method:"post",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/view", url: getPath() + "/ajax/view",
data: "{\"" + page + "\": {\"dir\": \"desc\"}}", data: "{\"" + page + "\": {\"dir\": \"desc\"}}",
}); });
var index = 0; var index = 0;
@@ -112,16 +116,18 @@ $("#desc").click(function() {
$("#asc").click(function() { $("#asc").click(function() {
if (direction === 1) { if (direction === 1) {
return; return;
} }
$("#desc").removeClass("active");
$("#asc").addClass("active");
var page = $(this).data("id"); var page = $(this).data("id");
$.ajax({ $.ajax({
method:"post", method:"post",
contentType: "application/json; charset=utf-8", contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../ajax/view", url: getPath() + "/ajax/view",
data: "{\"" + page + "\": {\"dir\": \"asc\"}}", data: "{\"" + page + "\": {\"dir\": \"asc\"}}",
}); });
var index = 0; var index = 0;
@@ -159,6 +165,8 @@ $("#asc").click(function() {
}); });
$("#all").click(function() { $("#all").click(function() {
$("#all").addClass("active");
$(".char").removeClass("active");
var cnt = $("#second").contents(); var cnt = $("#second").contents();
$("#list").append(cnt); $("#list").append(cnt);
// Find count of middle element // Find count of middle element
@@ -176,6 +184,9 @@ $("#all").click(function() {
}); });
$(".char").click(function() { $(".char").click(function() {
$(".char").removeClass("active");
$(this).addClass("active");
$("#all").removeClass("active");
var character = this.innerText; var character = this.innerText;
var count = 0; var count = 0;
var index = 0; var index = 0;
+6 -5
View File
@@ -28,16 +28,19 @@ $(function () {
function populateForm (book) { function populateForm (book) {
tinymce.get("description").setContent(book.description); 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) { $.each(book.tags, function(i, el) {
if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el); if ($.inArray(el, uniqueTags) === -1) uniqueTags.push(el);
}); });
var ampSeparatedAuthors = (book.authors || []).join(" & "); var ampSeparatedAuthors = (book.authors || []).join(" & ");
$("#bookAuthor").val(ampSeparatedAuthors); $("#bookAuthor").val(ampSeparatedAuthors);
$("#book_title").val(book.title); $("#book_title").val(book.title);
$("#tags").val(uniqueTags.join(",")); $("#tags").val(uniqueTags.join(", "));
$("#rating").data("rating").setValue(Math.round(book.rating)); $("#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 img").attr("src", book.cover);
$("#cover_url").val(book.cover); $("#cover_url").val(book.cover);
} }
@@ -125,9 +128,7 @@ $(function () {
e.preventDefault(); e.preventDefault();
keyword = $("#keyword").val(); keyword = $("#keyword").val();
$('.pill').each(function(){ $('.pill').each(function(){
// console.log($(this).data('control'));
$(this).data("initial", $(this).prop('checked')); $(this).data("initial", $(this).prop('checked'));
// console.log($(this).data('initial'));
}); });
doSearch(keyword); doSearch(keyword);
}); });
@@ -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);
+261
View File
@@ -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", "Redo": "\u91cd\u505a",
"Undo": "\u64a4\u9500", "Undo": "\u64a4\u9500",
"Cut": "\u526a\u5207", "Cut": "\u526a\u5207",
@@ -1,4 +1,4 @@
tinymce.addI18n('zh_TW',{ tinymce.addI18n('zh_Hant_TW',{
"Redo": "\u91cd\u505a", "Redo": "\u91cd\u505a",
"Undo": "\u64a4\u92b7", "Undo": "\u64a4\u92b7",
"Cut": "\u526a\u4e0b", "Cut": "\u526a\u4e0b",
Regular → Executable
+81 -19
View File
@@ -20,6 +20,20 @@ function getPath() {
return jsFileLocation.substr(0, jsFileLocation.search("/static/js/libs/jquery.min.js")); // the js folder path 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) { function elementSorter(a, b) {
a = +a.slice(0, -2); a = +a.slice(0, -2);
b = +b.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 // Syntax has to be bind not on, otherwise problems with firefox
$(".container-fluid").bind("dragenter dragover", function () { $(".container-fluid").bind("dragenter dragover", function () {
if($("#btn-upload").length && !$('body').hasClass('shelforder')) { if($("#btn-upload").length && !$('body').hasClass('shelforder')) {
@@ -168,18 +198,18 @@ function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
$confirm.modal('show'); $confirm.modal('show');
} }
$("#delete_confirm").click(function() { $("#delete_confirm").click(function(event) {
//get data-id attribute of the clicked element //get data-id attribute of the clicked element
var deleteId = $(this).data("delete-id"); var deleteId = $(this).data("delete-id");
var bookFormat = $(this).data("delete-format"); var bookFormat = $(this).data("delete-format");
var ajaxResponse = $(this).data("ajax"); var ajaxResponse = $(this).data("ajax");
if (bookFormat) { if (bookFormat) {
window.location.href = getPath() + "/delete/" + deleteId + "/" + bookFormat; postButton(event, getPath() + "/delete/" + deleteId + "/" + bookFormat);
} else { } else {
if (ajaxResponse) { if (ajaxResponse) {
path = getPath() + "/ajax/delete/" + deleteId; path = getPath() + "/ajax/delete/" + deleteId;
$.ajax({ $.ajax({
method:"get", method:"post",
url: path, url: path,
timeout: 900, timeout: 900,
success:function(data) { success:function(data) {
@@ -198,8 +228,7 @@ $("#delete_confirm").click(function() {
} }
}); });
} else { } else {
window.location.href = getPath() + "/delete/" + deleteId; postButton(event, getPath() + "/delete/" + deleteId);
} }
} }
@@ -284,11 +313,7 @@ $(function() {
} }
function fillFileTable(path, type, folder, filt) { 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({ $.ajax({
dataType: "json", dataType: "json",
data: { data: {
@@ -380,9 +405,11 @@ $(function() {
$("#restart").click(function() { $("#restart").click(function() {
$.ajax({ $.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../shutdown", url: getPath() + "/shutdown",
data: {"parameter":0}, data: JSON.stringify({"parameter":0}),
success: function success() { success: function success() {
$("#spinner").show(); $("#spinner").show();
setTimeout(restartTimer, 3000); setTimeout(restartTimer, 3000);
@@ -391,9 +418,11 @@ $(function() {
}); });
$("#shutdown").click(function() { $("#shutdown").click(function() {
$.ajax({ $.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: window.location.pathname + "/../../shutdown", url: getPath() + "/shutdown",
data: {"parameter":1}, data: JSON.stringify({"parameter":1}),
success: function success(data) { success: function success(data) {
return alert(data.text); return alert(data.text);
} }
@@ -451,9 +480,11 @@ $(function() {
$("#DialogContent").html(""); $("#DialogContent").html("");
$("#spinner2").show(); $("#spinner2").show();
$.ajax({ $.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: getPath() + "/shutdown", url: getPath() + "/shutdown",
data: {"parameter":2}, data: JSON.stringify({"parameter":2}),
success: function success(data) { success: function success(data) {
$("#spinner2").hide(); $("#spinner2").hide();
$("#DialogContent").html(data.text); $("#DialogContent").html(data.text);
@@ -504,6 +535,7 @@ $(function() {
$("#modal_kobo_token") $("#modal_kobo_token")
.on("show.bs.modal", function(e) { .on("show.bs.modal", function(e) {
$(e.relatedTarget).one('focus', function(e){$(this).blur();});
var $modalBody = $(this).find(".modal-body"); var $modalBody = $(this).find(".modal-body");
// Prevent static assets from loading multiple times // Prevent static assets from loading multiple times
@@ -521,6 +553,7 @@ $(function() {
.on("hidden.bs.modal", function() { .on("hidden.bs.modal", function() {
$(this).find(".modal-body").html("..."); $(this).find(".modal-body").html("...");
$("#config_delete_kobo_token").show(); $("#config_delete_kobo_token").show();
$("#kobo_full_sync").show();
}); });
$("#config_delete_kobo_token").click(function() { $("#config_delete_kobo_token").click(function() {
@@ -530,10 +563,11 @@ $(function() {
$(this).data('value'), $(this).data('value'),
function (value) { function (value) {
$.ajax({ $.ajax({
method: "get", method: "post",
url: getPath() + "/kobo_auth/deleteauthtoken/" + value, url: getPath() + "/kobo_auth/deleteauthtoken/" + value,
}); });
$("#config_delete_kobo_token").hide(); $("#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() { $("#user_submit").click(function() {
this.closest("form").submit(); this.closest("form").submit();
}); });
@@ -654,13 +715,14 @@ $(function() {
}); });
}); });
$("#delete_shelf").click(function() { $("#delete_shelf").click(function(event) {
confirmDialog( confirmDialog(
$(this).attr('id'), $(this).attr('id'),
"GeneralDeleteModal", "GeneralDeleteModal",
$(this).data('value'), $(this).data('value'),
function(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(""); $("#DialogContent").html("");
$("#spinner2").show(); $("#spinner2").show();
$.ajax({ $.ajax({
method:"get", method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json", dataType: "json",
url: getPath() + "/import_ldap_users", url: getPath() + "/import_ldap_users",
success: function success(data) { success: function success(data) {
@@ -743,4 +806,3 @@ $(function() {
}); });
}); });
}); });
+4 -1
View File
@@ -62,10 +62,13 @@ var reader;
}.bind(this)); }.bind(this));
} }
var csrftoken = $("input[name='csrf_token']").val();
// Save to database // Save to database
$.ajax(calibre.bookmarkUrl, { $.ajax(calibre.bookmarkUrl, {
method: "post", method: "post",
data: { bookmark: location || "" } data: { bookmark: location || "" },
headers: { "X-CSRFToken": csrftoken }
}).fail(function (xhr, status, error) { }).fail(function (xhr, status, error) {
alert(error); alert(error);
}); });
+9 -9
View File
@@ -47,15 +47,15 @@ $(function() {
var rows = rowsAfter; var rows = rowsAfter;
if (e.type === "uncheck-all") { if (e.type === "uncheck-all") {
rows = rowsBefore; selections = [];
} } else {
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) { var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
return row.id; return row.id;
}); });
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference"; var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids); selections = window._[func](selections, ids);
}
if (selections.length >= 2) { if (selections.length >= 2) {
$("#merge_books").removeClass("disabled"); $("#merge_books").removeClass("disabled");
$("#merge_books").attr("aria-disabled", false); $("#merge_books").attr("aria-disabled", false);
@@ -540,14 +540,14 @@ $(function() {
var rows = rowsAfter; var rows = rowsAfter;
if (e.type === "uncheck-all") { if (e.type === "uncheck-all") {
rows = rowsBefore; selections = [];
} } else {
var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) { var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) {
return row.id; return row.id;
}); });
var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference"; var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference";
selections = window._[func](selections, ids); selections = window._[func](selections, ids);
}
handle_header_buttons(); handle_header_buttons();
}); });
}); });
@@ -631,14 +631,14 @@ function singleUserFormatter(value, row) {
} }
function checkboxFormatter(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 + ')">'; 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 else
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', ' + this.column + ')">'; 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){ 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)">'; return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" checked onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', 0)">';
else else
return '<input type="checkbox" class="chk" data-pk="' + row.id + '" data-name="' + this.field + '" onchange="checkboxChange(this, ' + row.id + ', \'' + this.name + '\', 0)">'; 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) { function checkboxChange(checkbox, userId, field, field_index) {
$.ajax({ $.ajax({
method: "post", method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field, url: getPath() + "/ajax/editlistusers/" + field,
data: {"pk": userId, "field_index": field_index, "value": checkbox.checked}, data: {"pk": userId, "field_index": field_index, "value": checkbox.checked},
error: function(data) { error: function(data) {
handleListServerResponse([{type:"danger", message:data.responseText}]) handleListServerResponse([{type:"danger", message:data.responseText}])
+18 -6
View File
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
import os import os
import re import re
@@ -31,7 +30,8 @@ from cps import db
from cps import logger, config from cps import logger, config
from cps.subproc_wrapper import process_open from cps.subproc_wrapper import process_open
from flask_babel import gettext as _ 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.tasks.mail import TaskEmail
from cps import gdriveutils from cps import gdriveutils
@@ -147,10 +147,15 @@ class TaskConvert(CalibreTask):
try: try:
local_db.session.merge(new_format) local_db.session.merge(new_format)
local_db.session.commit() 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: except SQLAlchemyError as e:
local_db.session.rollback() local_db.session.rollback()
log.error("Database error: %s", e) log.error("Database error: %s", e)
local_db.session.close() local_db.session.close()
self._handleError(error_message)
return return
self.results['path'] = cur_book.path self.results['path'] = cur_book.path
self.title = cur_book.title self.title = cur_book.title
@@ -216,13 +221,16 @@ class TaskConvert(CalibreTask):
quotes.append(quotes_index) quotes.append(quotes_index)
quotes_index += 1 quotes_index += 1
p = process_open(command, quotes) p = process_open(command, quotes, newlines=False)
except OSError as e: except OSError as e:
return 1, _(u"Ebook-converter failed: %(error)s", error=e) return 1, _(u"Ebook-converter failed: %(error)s", error=e)
while p.poll() is None: while p.poll() is None:
nextline = p.stdout.readline() 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 # parse progress string from calibre-converter
progress = re.search(r"(\d+)%\s.*", nextline) progress = re.search(r"(\d+)%\s.*", nextline)
if progress: if progress:
@@ -235,11 +243,15 @@ class TaskConvert(CalibreTask):
calibre_traceback = p.stderr.readlines() calibre_traceback = p.stderr.readlines()
error_message = "" error_message = ""
for ele in calibre_traceback: 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'): 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 return check, error_message
@property @property
def name(self): def name(self):
return "Convert" return "Convert"
def __str__(self):
return "Convert {} {}".format(self.bookid, self.kindle_mail)
+7 -7
View File
@@ -158,10 +158,10 @@ class TaskEmail(CalibreTask):
else: else:
self.send_gmail_email(msg) self.send_gmail_email(msg)
except MemoryError as e: 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))) self._handleError(u'MemoryError sending e-mail: {}'.format(str(e)))
except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as 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"): if hasattr(e, "smtp_error"):
text = e.smtp_error.decode('utf-8').replace("\n", '. ') text = e.smtp_error.decode('utf-8').replace("\n", '. ')
elif hasattr(e, "message"): elif hasattr(e, "message"):
@@ -171,11 +171,11 @@ class TaskEmail(CalibreTask):
else: else:
text = '' text = ''
self._handleError(u'Smtplib Error sending e-mail: {}'.format(text)) self._handleError(u'Smtplib Error sending e-mail: {}'.format(text))
except socket.error as e: except (socket.error) as e:
log.debug_or_exception(e) log.debug_or_exception(e, stacklevel=3)
self._handleError(u'Socket Error sending e-mail: {}'.format(e.strerror)) self._handleError(u'Socket Error sending e-mail: {}'.format(e.strerror))
except Exception as ex: 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)) self._handleError(u'Error sending e-mail: {}'.format(ex))
def send_standard_email(self, msg): def send_standard_email(self, msg):
@@ -248,7 +248,7 @@ class TaskEmail(CalibreTask):
data = file_.read() data = file_.read()
file_.close() file_.close()
except IOError as e: 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?') log.error(u'The requested file could not be read. Maybe wrong permissions?')
return None return None
# Set mimetype # Set mimetype
@@ -267,4 +267,4 @@ class TaskEmail(CalibreTask):
return "E-mail" return "E-mail"
def __str__(self): def __str__(self):
return "{}, {}".format(self.name, self.subject) return "E-mail {}, {}".format(self.name, self.subject)
+3
View File
@@ -32,3 +32,6 @@ class TaskUpload(CalibreTask):
@property @property
def name(self): def name(self):
return "Upload" return "Upload"
def __str__(self):
return "Upload {}".format(self.message)
+5 -2
View File
@@ -16,7 +16,9 @@
<th>{{_('Downloads')}}</th> <th>{{_('Downloads')}}</th>
<th class="hidden-xs ">{{_('Admin')}}</th> <th class="hidden-xs ">{{_('Admin')}}</th>
<th class="hidden-xs hidden-sm">{{_('Password')}}</th> <th class="hidden-xs hidden-sm">{{_('Password')}}</th>
{% if config.config_uploading %}
<th class="hidden-xs hidden-sm">{{_('Upload')}}</th> <th class="hidden-xs hidden-sm">{{_('Upload')}}</th>
{% endif %}
<th class="hidden-xs hidden-sm">{{_('Download')}}</th> <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">{{_('View Books')}}</th>
<th class="hidden-xs hidden-sm hidden-md">{{_('Edit')}}</th> <th class="hidden-xs hidden-sm hidden-md">{{_('Edit')}}</th>
@@ -32,7 +34,9 @@
<td>{{user.downloads.count()}}</td> <td>{{user.downloads.count()}}</td>
<td class="hidden-xs">{{ display_bool_setting(user.role_admin()) }}</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> <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> <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">{{ 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_viewer()) }}</td>
<td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_edit()) }}</td> <td class="hidden-xs hidden-sm hidden-md">{{ display_bool_setting(user.role_edit()) }}</td>
@@ -184,9 +188,8 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
{% if feature_support['updater'] %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <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="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" 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> <div class="btn btn-primary hidden" id="perform_update" data-toggle="modal" data-target="#StatusDialog">{{_('Perform Update')}}</div>
+11 -11
View File
@@ -5,11 +5,11 @@
{% if author is not none %} {% if author is not none %}
<section class="author-bio"> <section class="author-bio">
{%if author.image_url is not none %} {%if author.image_url is not none %}
<img 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 %} {% endif %}
{%if author.about is not none %} {%if author.about is not none %}
<p>{{author.about|safe}}</p> <p>{{author.about}}</p>
{% endif %} {% endif %}
- {{_("via")}} <a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">Goodreads</a> - {{_("via")}} <a href="{{author.link}}" class="author-link" target="_blank" rel="noopener">Goodreads</a>
@@ -22,13 +22,13 @@
{% if author is not none %} {% if author is not none %}
<h3>{{_("In Library")}}</h3> <h3>{{_("In Library")}}</h3>
{% endif %} {% endif %}
<div class="filterheader hidden-xs hidden-sm"> <div class="filterheader hidden-xs">
<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="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" 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="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" 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="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" 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="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" 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_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" 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> <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>
<div class="row display-flex"> <div class="row display-flex">
{% if entries[0] %} {% if entries[0] %}
@@ -36,7 +36,7 @@
<div id="books" class="col-sm-3 col-lg-2 col-xs-6 book"> <div id="books" class="col-sm-3 col-lg-2 col-xs-6 book">
<div class="cover"> <div class="cover">
<a href="{{ url_for('web.show_book', book_id=entry.id) }}"> <a href="{{ url_for('web.show_book', book_id=entry.id) }}">
<span class="img" title="{{entry.title|safe}}"> <span class="img" title="{{entry.title}}">
<img src="{{ url_for('web.get_cover', book_id=entry.id) }}" /> <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 %} {% if entry.id in read_book_ids %}<span class="badge read glyphicon glyphicon-ok"></span>{% endif %}
</span> </span>
@@ -98,7 +98,7 @@
{% if other_books and author is not none %} {% if other_books and author is not none %}
<div class="discover"> <div class="discover">
<h3>{{_("More by")}} {{ author.name.replace('|',',')|safe }}</h3> <h3>{{_("More by")}} {{ author.name.replace('|',',') }}</h3>
<div class="row"> <div class="row">
{% for entry in other_books %} {% for entry in other_books %}
<div class="col-sm-3 col-lg-2 col-xs-6 book"> <div class="col-sm-3 col-lg-2 col-xs-6 book">
+9 -13
View File
@@ -98,8 +98,7 @@
<label for="rating">{{_('Rating')}}</label> <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 %}"> <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> </div>
{% if g.user.role_upload() or g.user.role_admin()%} {% if g.user.role_upload() and g.allow_upload %}
{% if g.allow_upload %}
<div class="form-group"> <div class="form-group">
<label for="cover_url">{{_('Fetch Cover from URL (JPEG - Image will be downloaded and stored in database)')}}</label> <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=""> <input type="text" class="form-control" name="cover_url" id="cover_url" value="">
@@ -110,7 +109,6 @@
<input id="btn-upload-cover" name="btn-upload-cover" type="file" accept=".jpg, .jpeg, .png, .webp"> <input id="btn-upload-cover" name="btn-upload-cover" type="file" accept=".jpg, .jpeg, .png, .webp">
</div> </div>
{% endif %} {% endif %}
{% endif %}
<label for="pubdate">{{_('Published Date')}}</label> <label for="pubdate">{{_('Published Date')}}</label>
<div class="form-group input-group"> <div class="form-group input-group">
<input type="text" class="datepicker form-control" name="pubdate" id="pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdateinput}}{% endif %}"> <input type="text" class="datepicker form-control" name="pubdate" id="pubdate" value="{% if book.pubdate %}{{book.pubdate|formatdateinput}}{% endif %}">
@@ -197,15 +195,13 @@
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if g.user.role_upload() or g.user.role_admin()%} {% if g.user.role_upload() and g.allow_upload %}
{% if g.allow_upload %}
<div role="group" aria-label="Upload new book format"> <div role="group" aria-label="Upload new book format">
<label class="btn btn-primary btn-file" for="btn-upload-format">{{ _('Upload Format') }}</label> <label class="btn btn-primary btn-file" for="btn-upload-format">{{ _('Upload Format') }}</label>
<div class="upload-format-input-text" id="upload-format"></div> <div class="upload-format-input-text" id="upload-format"></div>
<input id="btn-upload-format" name="btn-upload-format" type="file"> <input id="btn-upload-format" name="btn-upload-format" type="file">
</div> </div>
{% endif %} {% endif %}
{% endif %}
<div class="checkbox"> <div class="checkbox">
<label> <label>
@@ -230,7 +226,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="metaModalLabel">{{_('Fetch Metadata')}}</h4> <h4 class="modal-title text-center" id="metaModalLabel">{{_('Fetch Metadata')}}</h4>
<form class="padded-bottom" id="meta-search"> <form class="padded-bottom" id="meta-search">
<div class="input-group"> <div class="input-group">
<label class="sr-only" for="keyword">{{_('Keyword')}}</label> <label class="sr-only" for="keyword">{{_('Keyword')}}</label>
@@ -251,7 +247,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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> </div>
</div> </div>
@@ -269,17 +265,17 @@
> >
<div class="media-body"> <div class="media-body">
<h4 class="media-heading"> <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> </h4>
<p>{{_('Author')}}:<%= authors.join(" & ") %></p> <p class="meta_author">{{_('Author')}}:<%= authors.join(" & ") %></p>
<% if (publisher) { %> <% if (publisher) { %>
<p>{{_('Publisher')}}:<%= publisher %></p> <p class="meta_publisher">{{_('Publisher')}}:<%= publisher %></p>
<% } %> <% } %>
<% if (description) { %> <% if (description) { %>
<p>{{_('Description')}}: <%= description %></p> <p class="meta_description">{{_('Description')}}: <%= description %></p>
<% } %> <% } %>
<p>{{_('Source')}}: <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> </p>
</div> </div>
</li> </li>
+2 -2
View File
@@ -120,8 +120,8 @@
<div class="text-left" id="merge_to"></div> <div class="text-left" id="merge_to"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<input type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal"> <input id="merge_confirm" 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> <button id="merge_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -105,7 +105,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}> <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>
<div data-related="upload_settings"> <div data-related="upload_settings">
<div class="form-group"> <div class="form-group">
+5 -1
View File
@@ -95,18 +95,22 @@
<input type="checkbox" name="viewer_role" id="viewer_role" {% if conf.role_viewer() %}checked{% endif %}> <input type="checkbox" name="viewer_role" id="viewer_role" {% if conf.role_viewer() %}checked{% endif %}>
<label for="viewer_role">{{_('Allow eBook Viewer')}}</label> <label for="viewer_role">{{_('Allow eBook Viewer')}}</label>
</div> </div>
{% if conf.config_uploading %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="upload_role" id="upload_role" {% if conf.role_upload() %}checked{% endif %}> <input type="checkbox" name="upload_role" id="upload_role" {% if conf.role_upload() %}checked{% endif %}>
<label for="upload_role">{{_('Allow Uploads')}}</label> <label for="upload_role">{{_('Allow Uploads')}}</label>
</div> </div>
{% endif %}
<div class="form-group"> <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> <label for="edit_role">{{_('Allow Edit')}}</label>
</div> </div>
<div data-related="edit_settings">
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="delete_role" id="delete_role" {% if conf.role_delete_books() %}checked{% endif %}> <input type="checkbox" name="delete_role" id="delete_role" {% if conf.role_delete_books() %}checked{% endif %}>
<label for="delete_role">{{_('Allow Delete Books')}}</label> <label for="delete_role">{{_('Allow Delete Books')}}</label>
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="passwd_role" id="passwd_role" {% if conf.role_passwd() %}checked{% endif %}> <input type="checkbox" name="passwd_role" id="passwd_role" {% if conf.role_passwd() %}checked{% endif %}>
<label for="passwd_role">{{_('Allow Changing Password')}}</label> <label for="passwd_role">{{_('Allow Changing Password')}}</label>
+2 -2
View File
@@ -38,7 +38,7 @@
{% endif %} {% endif %}
{% if g.user.kindle_mail and kindle_list %} {% if g.user.kindle_mail and kindle_list %}
{% if kindle_list.__len__() == 1 %} {% 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 %} {% else %}
<div class="btn-group" role="group"> <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"> <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> </button>
<ul class="dropdown-menu" aria-labelledby="send-to-kindle"> <ul class="dropdown-menu" aria-labelledby="send-to-kindle">
{% for format in kindle_list %} {% 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%} {%endfor%}
</ul> </ul>
</div> </div>
+9 -6
View File
@@ -1,12 +1,15 @@
{% extends "fragment.html" %} {% extends "fragment.html" %}
{% block body %} {% block body %}
<div class="well"> <div class="well">
<p> <p>
{{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}</a> {% 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>
<p>
{% if not warning %}api_endpoint={{kobo_auth_url}}{% else %}{{warning}}{% endif %}</a>
</p>
<p>
</div> </div>
{% endblock %} {% endblock %}
+11 -12
View File
@@ -2,23 +2,22 @@
{% block body %} {% block body %}
<h1 class="{{page}}">{{_(title)}}</h1> <h1 class="{{page}}">{{_(title)}}</h1>
<div class="filterheader hidden-xs hidden-sm"> <div class="filterheader hidden-xs">
{% if entries.__len__() %} {% if entries.__len__() and data == 'author' %}
{% if entries[0][0].sort %} <div id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></div>
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button>
{% endif %} {% endif %}
{% endif %} <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button id="asc" data-id="series" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button> <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>
<button id="desc" data-id="series" data-order="{{ order }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button> <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 %} {% 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 %} {% 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%} {% for char in charlist%}
<button class="btn btn-primary char">{{char.char}}</button> <div class="btn btn-primary char">{{char.char}}</div>
{% endfor %} {% endfor %}
</div> </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> </div>
{% if entries[0] %} {% if entries[0] %}
@@ -28,7 +27,7 @@
<div class="cover"> <div class="cover">
<a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}"> <a href="{{url_for('web.books_list', data=data, sort_param='stored', book_id=entry[0].series[0].id )}}">
<span class="img" title="{{entry[0].series[0].name}}"> <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 class="badge">{{entry.count}}</span>
</span> </span>
</a> </a>
+16 -11
View File
@@ -64,18 +64,23 @@
{% endif %} {% endif %}
<div class="discover load-more"> <div class="discover load-more">
<h2 class="{{title}}">{{title}}</h2> <h2 class="{{title}}">{{title}}</h2>
<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" 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> {% if page == 'hot' %}
<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 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 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 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>
<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> {% else %}
<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 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 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 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 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 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 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> <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' %} {% 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 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" 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 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 %} {% endif %}
</div> </div>
+21 -5
View File
@@ -1,19 +1,35 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block body %} {% block body %}
<h1>{{title}}</h1> <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="container">
<div class="col-xs-12 col-sm-6"> <div div id="list" class="col-xs-12 col-sm-6">
{% for lang in languages %} {% for lang in languages %}
{% if loop.index0 == (loop.length/2)|int and loop.length > 20 %} {% if loop.index0 == (loop.length/2)|int and loop.length > 20 %}
</div> </div>
<div class="col-xs-12 col-sm-6"> <div id="second" class="col-xs-12 col-sm-6">
{% endif %} {% endif %}
<div class="row"> <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_counter[loop.index0].bookcount}}</span></div> <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.lang_code, data=data, sort_param='new')}}">{{lang.name}}</a></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> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block js %}
<script src="{{ url_for('static', filename='js/filter_list.js') }}"></script>
{% endblock %}
+2 -4
View File
@@ -58,8 +58,7 @@
{% endif %} {% endif %}
<ul class="nav navbar-nav navbar-right" id="main-nav"> <ul class="nav navbar-nav navbar-right" id="main-nav">
{% if g.user.is_authenticated or g.allow_anonymous %} {% if g.user.is_authenticated or g.allow_anonymous %}
{% if g.user.role_upload() or g.user.role_admin()%} {% if g.user.role_upload() and g.allow_upload %}
{% if g.allow_upload %}
<li> <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"> <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() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
@@ -70,7 +69,6 @@
</form> </form>
</li> </li>
{% endif %} {% endif %}
{% endif %}
{% if not g.user.is_anonymous %} {% 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> <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>
{% endif %} {% endif %}
@@ -185,7 +183,7 @@
</div> </div>
<div class="modal-body">...</div> <div class="modal-body">...</div>
<div class="modal-footer"> <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> </div>
</div> </div>
+9 -11
View File
@@ -2,25 +2,23 @@
{% block body %} {% block body %}
<h1 class="{{page}}">{{_(title)}}</h1> <h1 class="{{page}}">{{_(title)}}</h1>
<div class="filterheader hidden-xs hidden-sm"> <div class="filterheader hidden-xs">
{% if entries.__len__() %} <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if data == 'author' %} {% if entries.__len__() and data == 'author' %}
<button id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></button> <div id="sort_name" class="btn btn-primary"><b>B,A <-> A B</b></div>
{% endif %} {% endif %}
{% endif %} <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>
<button id="asc" data-order="{{ order }}" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet"></span></button> <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>
<button id="desc" data-id="{{ data }}" class="btn btn-primary"><span class="glyphicon glyphicon-sort-by-alphabet-alt"></span></button>
{% if charlist|length %} {% 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 %} {% 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%} {% for char in charlist%}
<button class="btn btn-primary char">{{char.char}}</button> <div class="btn btn-primary char">{{char.char}}</div>
{% endfor %} {% endfor %}
</div> </div>
{% if data == "series" %} {% 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> <button class="update-view btn btn-primary" data-target="series_view" id="grid-button" data-view="grid">Grid</button>
{% endif %} {% endif %}
</div> </div>
+1
View File
@@ -17,6 +17,7 @@
<body> <body>
<div id="sidebar"> <div id="sidebar">
<div id="panels"> <div id="panels">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!--input id="searchBox" placeholder="search" type="search"--> <!--input id="searchBox" placeholder="search" type="search"-->
<!--a id="show-Search" class="show_view icon-search" data-view="Search">Search</a--> <!--a id="show-Search" class="show_view icon-search" data-view="Search">Search</a-->
+11 -10
View File
@@ -9,6 +9,7 @@
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}
{% if g.user.shelf.all() or g.shelves_access %} {% if g.user.shelf.all() or g.shelves_access %}
<div id="shelf-actions" class="btn-toolbar" role="toolbar"> <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"> <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"> <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')}} <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"> <ul id="add-to-shelves" class="dropdown-menu" aria-labelledby="add-to-shelf">
{% for shelf in g.shelves_access %} {% for shelf in g.shelves_access %}
{% if not shelf.is_public or g.user.role_edit_shelfs() %} {% 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 %} {% endif %}
{%endfor%} {%endfor%}
</ul> </ul>
@@ -25,15 +26,15 @@
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
<div class="filterheader hidden-xs hidden-sm"><!-- ToDo: Implement filter for search results --> <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" href="{{url_for('web.books_list', data=page, sort_param='new', query=query)}}"><span class="glyphicon glyphicon-sort-by-order"></span></a> <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" 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="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" 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="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" 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="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" 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_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" 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="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" 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_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" 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> <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> </div>
{% endif %} {% endif %}
+4 -18
View File
@@ -2,18 +2,20 @@
{% block body %} {% block body %}
<div class="discover"> <div class="discover">
<h2>{{title}}</h2> <h2>{{title}}</h2>
<!--form method="post"--->
{% if g.user.role_download() %} {% 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> <a id="shelf_down" href="{{ url_for('shelf.show_simpleshelf', shelf_id=shelf.id) }}" class="btn btn-primary">{{ _('Download') }} </a>
{% endif %} {% endif %}
{% if g.user.is_authenticated %} {% if g.user.is_authenticated %}
{% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %} {% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %}
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <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> <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> <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__() %} {% 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> <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> <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, 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 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> <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 %} {% endfor %}
</div> </div>
</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 %} {% endblock %}
{% block modal %} {% block modal %}
{{ delete_confirm_modal() }} {{ delete_confirm_modal() }}
+9 -2
View File
@@ -17,7 +17,7 @@
</div> </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_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 ) %} {% 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 %} {% endif %}
<div class="form-group"> <div class="form-group">
<label for="password">{{_('Password')}}</label> <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> <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 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>
<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 %} {% endif %}
<div class="col-sm-6"> <div class="col-sm-6">
{% for element in sidebar %} {% for element in sidebar %}
@@ -101,18 +104,22 @@
<input type="checkbox" name="viewer_role" id="viewer_role" {% if content.role_viewer() %}checked{% endif %}> <input type="checkbox" name="viewer_role" id="viewer_role" {% if content.role_viewer() %}checked{% endif %}>
<label for="viewer_role">{{_('Allow eBook Viewer')}}</label> <label for="viewer_role">{{_('Allow eBook Viewer')}}</label>
</div> </div>
{% if config.config_uploading %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="upload_role" id="upload_role" {% if content.role_upload() %}checked{% endif %}> <input type="checkbox" name="upload_role" id="upload_role" {% if content.role_upload() %}checked{% endif %}>
<label for="upload_role">{{_('Allow Uploads')}}</label> <label for="upload_role">{{_('Allow Uploads')}}</label>
</div> </div>
{% endif %}
<div class="form-group"> <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> <label for="edit_role">{{_('Allow Edit')}}</label>
</div> </div>
<div data-related="edit_settings">
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="delete_role" id="delete_role" {% if content.role_delete_books() %}checked{% endif %}> <input type="checkbox" name="delete_role" id="delete_role" {% if content.role_delete_books() %}checked{% endif %}>
<label for="delete_role">{{_('Allow Delete Books')}}</label> <label for="delete_role">{{_('Allow Delete Books')}}</label>
</div> </div>
</div>
{% if not content.role_anonymous() %} {% if not content.role_anonymous() %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}checked{% endif %}> <input type="checkbox" name="passwd_role" id="passwd_role" {% if content.role_passwd() %}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