From e0229c917cf627a1f3bacb35e7f8bc4d89c49833 Mon Sep 17 00:00:00 2001 From: otapi <31888571+otapi@users.noreply.github.com> Date: Wed, 10 Oct 2018 23:01:05 +0200 Subject: [PATCH 001/349] Download only shelf Show only titles and download button for specific shelf. Currently only direct link works, e.g: calibre-web/shelfdown/6 --- cps/templates/shelfdown.html | 82 ++++++++++++++++++++++++++++++++++++ cps/web.py | 29 +++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 cps/templates/shelfdown.html diff --git a/cps/templates/shelfdown.html b/cps/templates/shelfdown.html new file mode 100644 index 00000000..32fa78b2 --- /dev/null +++ b/cps/templates/shelfdown.html @@ -0,0 +1,82 @@ + + + + {{instance}} | {{title}} + + + + + + + + + + + {% if g.user.get_theme == 1 %} + + {% endif %} + + + + + {% block header %}{% endblock %} + + +{% block body %} +
+

{{title}}

+
+ + {% for entry in entries %} +
+ +
+

{{entry.title|shortentitle}}

+

+ {% for author in entry.authors %} + {{author.name.replace('|',',')}} + {% if not loop.last %} + & + {% endif %} + {% endfor %} +

+ +
+ +
+ {% if g.user.role_download() %} + {% if entry.data|length %} +
+ {% if entry.data|length < 2 %} + + {% for format in entry.data %} + + {{format.format}} ({{ format.uncompressed_size|filesizeformat }}) + + {% endfor %} + {% else %} + + + {% endif %} +
+ {% endif %} + {% endif %} +
+
+ {% endfor %} +
+
+ +{% endblock %} + + \ No newline at end of file diff --git a/cps/web.py b/cps/web.py index 5d6e766c..b18a2ecc 100644 --- a/cps/web.py +++ b/cps/web.py @@ -2676,6 +2676,35 @@ def show_shelf(shelf_id): return redirect(url_for("index")) +@app.route("/shelfdown/") +@login_required_if_no_ano +def show_shelf_down(shelf_id): + if current_user.is_anonymous: + shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() + else: + shelf = ub.session.query(ub.Shelf).filter(ub.or_(ub.and_(ub.Shelf.user_id == int(current_user.id), + ub.Shelf.id == shelf_id), + ub.and_(ub.Shelf.is_public == 1, + ub.Shelf.id == shelf_id))).first() + result = list() + # user is allowed to access shelf + if shelf: + books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).order_by( + ub.BookShelf.order.asc()).all() + for book in books_in_shelf: + cur_book = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() + if cur_book: + result.append(cur_book) + else: + app.logger.info('Not existing book %s in shelf %s deleted' % (book.book_id, shelf.id)) + ub.session.query(ub.BookShelf).filter(ub.BookShelf.book_id == book.book_id).delete() + ub.session.commit() + return render_title_template('shelfdown.html', entries=result, title=_(u"Shelf: '%(name)s'", name=shelf.name), + shelf=shelf, page="shelf") + else: + flash(_(u"Error opening shelf. Shelf does not exist or is not accessible"), category="error") + return redirect(url_for("index")) + @app.route("/shelf/order/", methods=["GET", "POST"]) @login_required def order_shelf(shelf_id): From c6d3613e576aa8d0cfa943df5ad51f53c749219d Mon Sep 17 00:00:00 2001 From: otapi <31888571+otapi@users.noreply.github.com> Date: Thu, 11 Oct 2018 18:20:38 +0200 Subject: [PATCH 002/349] Add UI link button to shelves --- cps/templates/shelf.html | 6 +++++- cps/web.py | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cps/templates/shelf.html b/cps/templates/shelf.html index 28cd20bb..645dad74 100644 --- a/cps/templates/shelf.html +++ b/cps/templates/shelf.html @@ -2,9 +2,13 @@ {% block body %}

{{title}}

+ {% if g.user.role_download() %} + {{ _('Download') }} + {% endif %} {% if g.user.is_authenticated %} {% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %} -
{{ _('Delete this Shelf') }}
+ +
{{ _('Delete this Shelf') }}
{{ _('Edit Shelf') }} {{ _('Change order') }} {% endif %} diff --git a/cps/web.py b/cps/web.py index b18a2ecc..d8d4bab1 100644 --- a/cps/web.py +++ b/cps/web.py @@ -2677,7 +2677,6 @@ def show_shelf(shelf_id): @app.route("/shelfdown/") -@login_required_if_no_ano def show_shelf_down(shelf_id): if current_user.is_anonymous: shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first() From 9b4ca22254d0ce0b3f6c0d8c7e36a68d55997e44 Mon Sep 17 00:00:00 2001 From: otapi <31888571+otapi@users.noreply.github.com> Date: Thu, 11 Oct 2018 18:39:31 +0200 Subject: [PATCH 003/349] Update shelf.html --- cps/templates/shelf.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cps/templates/shelf.html b/cps/templates/shelf.html index 645dad74..e7d528b3 100644 --- a/cps/templates/shelf.html +++ b/cps/templates/shelf.html @@ -7,8 +7,7 @@ {% endif %} {% if g.user.is_authenticated %} {% if (g.user.role_edit_shelfs() and shelf.is_public ) or not shelf.is_public %} - -
{{ _('Delete this Shelf') }}
+
{{ _('Delete this Shelf') }}
{{ _('Edit Shelf') }} {{ _('Change order') }} {% endif %} From 1abbcfa3c6e1f5c3f4db4d4fe513d25d4b49f350 Mon Sep 17 00:00:00 2001 From: Jim Ma Date: Thu, 11 Oct 2018 19:52:30 +0800 Subject: [PATCH 004/349] Add OAuth support: GitHub & Google --- cps/oauth.py | 134 ++++++++++++++++ cps/templates/config_edit.html | 30 ++++ cps/templates/login.html | 17 +- cps/templates/register.html | 15 ++ cps/ub.py | 35 +++++ cps/web.py | 279 ++++++++++++++++++++++++++++++++- requirements.txt | 2 + 7 files changed, 507 insertions(+), 5 deletions(-) create mode 100644 cps/oauth.py diff --git a/cps/oauth.py b/cps/oauth.py new file mode 100644 index 00000000..679e7f31 --- /dev/null +++ b/cps/oauth.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from flask import session +from flask_dance.consumer.backend.sqla import SQLAlchemyBackend, first, _get_real_user +from sqlalchemy.orm.exc import NoResultFound + + +class OAuthBackend(SQLAlchemyBackend): + """ + Stores and retrieves OAuth tokens using a relational database through + the `SQLAlchemy`_ ORM. + + .. _SQLAlchemy: http://www.sqlalchemy.org/ + """ + def __init__(self, model, session, + user=None, user_id=None, user_required=None, anon_user=None, + cache=None): + super(OAuthBackend, self).__init__(model, session, user, user_id, user_required, anon_user, cache) + + def get(self, blueprint, user=None, user_id=None): + if blueprint.name + '_oauth_token' in session and session[blueprint.name + '_oauth_token'] != '': + return session[blueprint.name + '_oauth_token'] + # check cache + cache_key = self.make_cache_key(blueprint=blueprint, user=user, user_id=user_id) + token = self.cache.get(cache_key) + if token: + return token + + # if not cached, make database queries + query = ( + self.session.query(self.model) + .filter_by(provider=blueprint.name) + ) + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) + + use_provider_user_id = False + if blueprint.name + '_oauth_user_id' in session and session[blueprint.name + '_oauth_user_id'] != '': + query = query.filter_by(provider_user_id=session[blueprint.name + '_oauth_user_id']) + use_provider_user_id = True + + if self.user_required and not u and not uid and not use_provider_user_id: + #raise ValueError("Cannot get OAuth token without an associated user") + return None + # check for user ID + if hasattr(self.model, "user_id") and uid: + query = query.filter_by(user_id=uid) + # check for user (relationship property) + elif hasattr(self.model, "user") and u: + query = query.filter_by(user=u) + # if we have the property, but not value, filter by None + elif hasattr(self.model, "user_id"): + query = query.filter_by(user_id=None) + # run query + try: + token = query.one().token + except NoResultFound: + token = None + + # cache the result + self.cache.set(cache_key, token) + + return token + + def set(self, blueprint, token, user=None, user_id=None): + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) + + if self.user_required and not u and not uid: + raise ValueError("Cannot set OAuth token without an associated user") + + # if there was an existing model, delete it + existing_query = ( + self.session.query(self.model) + .filter_by(provider=blueprint.name) + ) + # check for user ID + has_user_id = hasattr(self.model, "user_id") + if has_user_id and uid: + existing_query = existing_query.filter_by(user_id=uid) + # check for user (relationship property) + has_user = hasattr(self.model, "user") + if has_user and u: + existing_query = existing_query.filter_by(user=u) + # queue up delete query -- won't be run until commit() + existing_query.delete() + # create a new model for this token + kwargs = { + "provider": blueprint.name, + "token": token, + } + if has_user_id and uid: + kwargs["user_id"] = uid + if has_user and u: + kwargs["user"] = u + self.session.add(self.model(**kwargs)) + # commit to delete and add simultaneously + self.session.commit() + # invalidate cache + self.cache.delete(self.make_cache_key( + blueprint=blueprint, user=user, user_id=user_id + )) + + def delete(self, blueprint, user=None, user_id=None): + query = ( + self.session.query(self.model) + .filter_by(provider=blueprint.name) + ) + uid = first([user_id, self.user_id, blueprint.config.get("user_id")]) + u = first(_get_real_user(ref, self.anon_user) + for ref in (user, self.user, blueprint.config.get("user"))) + + if self.user_required and not u and not uid: + raise ValueError("Cannot delete OAuth token without an associated user") + + # check for user ID + if hasattr(self.model, "user_id") and uid: + query = query.filter_by(user_id=uid) + # check for user (relationship property) + elif hasattr(self.model, "user") and u: + query = query.filter_by(user=u) + # if we have the property, but not value, filter by None + elif hasattr(self.model, "user_id"): + query = query.filter_by(user_id=None) + # run query + query.delete() + self.session.commit() + # invalidate cache + self.cache.delete(self.make_cache_key( + blueprint=blueprint, user=user, user_id=user_id, + )) diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index b2826b39..0979e465 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -162,6 +162,36 @@
{% endif %} +
+ + + {{_('Obtain GitHub OAuth Credentail')}} +
+
+
+ + +
+
+ + +
+
+
+ + + {{_('Obtain Google OAuth Credentail')}} +
+
+
+ + +
+
+ + +
+
diff --git a/cps/templates/login.html b/cps/templates/login.html index 3e8ebe1e..8e622079 100644 --- a/cps/templates/login.html +++ b/cps/templates/login.html @@ -18,9 +18,24 @@ - {% if remote_login %} + {% if config.config_remote_login %} {{_('Log in with magic link')}} {% endif %} + {% if config.config_use_github_oauth %} + + + + {% endif %} + {% if config.config_use_google_oauth %} + + + + {% endif %} {% if error %} diff --git a/cps/templates/register.html b/cps/templates/register.html index 70bd10c7..24edc3a2 100644 --- a/cps/templates/register.html +++ b/cps/templates/register.html @@ -12,6 +12,21 @@ + {% if config.config_use_github_oauth %} + + + + {% endif %} + {% if config.config_use_google_oauth %} + + + + {% endif %} {% if error %} diff --git a/cps/ub.py b/cps/ub.py index f1b19d02..8811b972 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -6,6 +6,7 @@ from sqlalchemy import exc from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import * from flask_login import AnonymousUserMixin +from flask_dance.consumer.backend.sqla import OAuthConsumerMixin import sys import os import logging @@ -169,6 +170,12 @@ class User(UserBase, Base): theme = Column(Integer, default=0) +class OAuth(OAuthConsumerMixin, Base): + provider_user_id = Column(String(256)) + user_id = Column(Integer, ForeignKey(User.id)) + user = relationship(User) + + # Class for anonymous user is derived from User base and completly overrides methods and properties for the # anonymous user class Anonymous(AnonymousUserMixin, UserBase): @@ -306,6 +313,12 @@ class Settings(Base): config_use_goodreads = Column(Boolean) config_goodreads_api_key = Column(String) config_goodreads_api_secret = Column(String) + config_use_github_oauth = Column(Boolean) + config_github_oauth_client_id = Column(String) + config_github_oauth_client_secret = Column(String) + config_use_google_oauth = Column(Boolean) + config_google_oauth_client_id = Column(String) + config_google_oauth_client_secret = Column(String) config_mature_content_tags = Column(String) config_logfile = Column(String) config_ebookconverter = Column(Integer, default=0) @@ -378,6 +391,12 @@ class Config: self.config_use_goodreads = data.config_use_goodreads self.config_goodreads_api_key = data.config_goodreads_api_key self.config_goodreads_api_secret = data.config_goodreads_api_secret + self.config_use_github_oauth = data.config_use_github_oauth + self.config_github_oauth_client_id = data.config_github_oauth_client_id + self.config_github_oauth_client_secret = data.config_github_oauth_client_secret + self.config_use_google_oauth = data.config_use_google_oauth + self.config_google_oauth_client_id = data.config_google_oauth_client_id + self.config_google_oauth_client_secret = data.config_google_oauth_client_secret if data.config_mature_content_tags: self.config_mature_content_tags = data.config_mature_content_tags else: @@ -661,6 +680,22 @@ def migrate_Database(): conn.execute("ALTER TABLE Settings ADD column `config_calibre` String DEFAULT ''") session.commit() + try: + session.query(exists().where(Settings.config_use_github_oauth)).scalar() + except exc.OperationalError: + conn = engine.connect() + conn.execute("ALTER TABLE Settings ADD column `config_use_github_oauth` INTEGER DEFAULT 0") + conn.execute("ALTER TABLE Settings ADD column `config_github_oauth_client_id` String DEFAULT ''") + conn.execute("ALTER TABLE Settings ADD column `config_github_oauth_client_secret` String DEFAULT ''") + session.commit() + try: + session.query(exists().where(Settings.config_use_google_oauth)).scalar() + except exc.OperationalError: + conn = engine.connect() + conn.execute("ALTER TABLE Settings ADD column `config_use_google_oauth` INTEGER DEFAULT 0") + conn.execute("ALTER TABLE Settings ADD column `config_google_oauth_client_id` String DEFAULT ''") + conn.execute("ALTER TABLE Settings ADD column `config_google_oauth_client_secret` String DEFAULT ''") + session.commit() # Remove login capability of user Guest conn = engine.connect() conn.execute("UPDATE user SET password='' where nickname = 'Guest' and password !=''") diff --git a/cps/web.py b/cps/web.py index 5d6e766c..0937fdb7 100644 --- a/cps/web.py +++ b/cps/web.py @@ -4,7 +4,7 @@ import mimetypes import logging from logging.handlers import RotatingFileHandler -from flask import (Flask, render_template, request, Response, redirect, +from flask import (Flask, session, render_template, request, Response, redirect, url_for, send_from_directory, make_response, g, flash, abort, Markup) from flask import __version__ as flaskVersion @@ -55,6 +55,11 @@ from redirect import redirect_back import time import server from reverseproxy import ReverseProxied +from flask_dance.contrib.github import make_github_blueprint, github +from flask_dance.contrib.google import make_google_blueprint, google +from flask_dance.consumer import oauth_authorized, oauth_error +from sqlalchemy.orm.exc import NoResultFound +from oauth import OAuthBackend try: from googleapiclient.errors import HttpError except ImportError: @@ -114,6 +119,7 @@ EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit' # EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + (['rar','cbr'] if rar_support else [])) +oauth_check = [] '''class ReverseProxied(object): """Wrap the application in this middleware and configure the @@ -348,6 +354,35 @@ def remote_login_required(f): return inner +def github_oauth_required(f): + @wraps(f) + def inner(*args, **kwargs): + if config.config_use_github_oauth: + return f(*args, **kwargs) + if request.is_xhr: + data = {'status': 'error', 'message': 'Not Found'} + response = make_response(json.dumps(data, ensure_ascii=False)) + response.headers["Content-Type"] = "application/json; charset=utf-8" + return response, 404 + abort(404) + + return inner + + +def google_oauth_required(f): + @wraps(f) + def inner(*args, **kwargs): + if config.config_use_google_oauth: + return f(*args, **kwargs) + if request.is_xhr: + data = {'status': 'error', 'message': 'Not Found'} + response = make_response(json.dumps(data, ensure_ascii=False)) + response.headers["Content-Type"] = "application/json; charset=utf-8" + return response, 404 + abort(404) + + return inner + # custom jinja filters # pagination links in jinja @@ -2264,6 +2299,7 @@ def register(): try: ub.session.add(content) ub.session.commit() + register_user_with_oauth(content) helper.send_registration_mail(to_save["email"],to_save["nickname"], password) except Exception: ub.session.rollback() @@ -2279,7 +2315,8 @@ def register(): flash(_(u"This username or e-mail address is already in use."), category="error") return render_title_template('register.html', title=_(u"register"), page="register") - return render_title_template('register.html', title=_(u"register"), page="register") + register_user_with_oauth() + return render_title_template('register.html', config=config, title=_(u"register"), page="register") @app.route('/login', methods=['GET', 'POST']) @@ -2304,8 +2341,7 @@ def login(): # if next_url is None or not is_safe_url(next_url): next_url = url_for('index') - return render_title_template('login.html', title=_(u"login"), next_url=next_url, - remote_login=config.config_remote_login, page="login") + return render_title_template('login.html', title=_(u"login"), next_url=next_url, config=config, page="login") @app.route('/logout') @@ -2313,6 +2349,7 @@ def login(): def logout(): if current_user is not None and current_user.is_authenticated: logout_user() + logout_oauth_user() return redirect(url_for('login')) @@ -3019,6 +3056,29 @@ def configuration_helper(origin): content.config_goodreads_api_key = to_save["config_goodreads_api_key"] if "config_goodreads_api_secret" in to_save: content.config_goodreads_api_secret = to_save["config_goodreads_api_secret"] + + # GitHub OAuth configuration + content.config_use_github_oauth = ("config_use_github_oauth" in to_save and to_save["config_use_github_oauth"] == "on") + if "config_github_oauth_client_id" in to_save: + content.config_github_oauth_client_id = to_save["config_github_oauth_client_id"] + if "config_github_oauth_client_secret" in to_save: + content.config_github_oauth_client_secret = to_save["config_github_oauth_client_secret"] + + if content.config_github_oauth_client_id != config.config_github_oauth_client_id or \ + content.config_github_oauth_client_secret != config.config_github_oauth_client_secret: + reboot_required = True + + # Google OAuth configuration + content.config_use_google_oauth = ("config_use_google_oauth" in to_save and to_save["config_use_google_oauth"] == "on") + if "config_google_oauth_client_id" in to_save: + content.config_google_oauth_client_id = to_save["config_google_oauth_client_id"] + if "config_google_oauth_client_secret" in to_save: + content.config_google_oauth_client_secret = to_save["config_google_oauth_client_secret"] + + if content.config_google_oauth_client_id != config.config_google_oauth_client_id or \ + content.config_google_oauth_client_secret != config.config_google_oauth_client_secret: + reboot_required = True + if "config_log_level" in to_save: content.config_log_level = int(to_save["config_log_level"]) if content.config_logfile != to_save["config_logfile"]: @@ -3883,3 +3943,214 @@ def convert_bookformat(book_id): else: flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") return redirect(request.environ["HTTP_REFERER"]) + + +def register_oauth_blueprint(blueprint): + if blueprint.name != "": + oauth_check.append(blueprint.name) + + +def register_user_with_oauth(user=None): + all_oauth = [] + for oauth in oauth_check: + if oauth + '_oauth_user_id' in session and session[oauth + '_oauth_user_id'] != '': + all_oauth.append(oauth) + if len(all_oauth) == 0: + return + if user is None: + flash(_(u"Register with %s" % ", ".join(all_oauth)), category="success") + else: + for oauth in all_oauth: + # Find this OAuth token in the database, or create it + query = ub.session.query(ub.OAuth).filter_by( + provider=oauth, + provider_user_id=session[oauth + "_oauth_user_id"], + ) + try: + oauth = query.one() + oauth.user_id = user.id + except NoResultFound: + # no found, return error + return + try: + ub.session.commit() + except Exception as e: + app.logger.exception(e) + ub.session.rollback() + + +def logout_oauth_user(): + for oauth in oauth_check: + if oauth + '_oauth_user_id' in session: + session.pop(oauth + '_oauth_user_id') + + +github_blueprint = make_github_blueprint( + client_id=config.config_github_oauth_client_id, + client_secret=config.config_github_oauth_client_secret, + redirect_to="github_login",) + +google_blueprint = make_google_blueprint( + client_id=config.config_google_oauth_client_id, + client_secret=config.config_google_oauth_client_secret, + redirect_to="google_login", + scope=[ + "https://www.googleapis.com/auth/plus.me", + "https://www.googleapis.com/auth/userinfo.email", + ] +) + +app.register_blueprint(google_blueprint, url_prefix="/login") +app.register_blueprint(github_blueprint, url_prefix='/login') + +github_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) +google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) + +register_oauth_blueprint(github_blueprint) +register_oauth_blueprint(google_blueprint) + + +@oauth_authorized.connect_via(github_blueprint) +def github_logged_in(blueprint, token): + if not token: + flash("Failed to log in with GitHub.", category="error") + return False + + resp = blueprint.session.get("/user") + if not resp.ok: + msg = "Failed to fetch user info from GitHub." + flash(msg, category="error") + return False + + github_info = resp.json() + github_user_id = str(github_info["id"]) + return oauth_update_token(blueprint, token, github_user_id) + + +@oauth_authorized.connect_via(google_blueprint) +def google_logged_in(blueprint, token): + if not token: + flash("Failed to log in with Google.", category="error") + return False + + resp = blueprint.session.get("/oauth2/v2/userinfo") + if not resp.ok: + msg = "Failed to fetch user info from Google." + flash(msg, category="error") + return False + + google_info = resp.json() + google_user_id = str(google_info["id"]) + + return oauth_update_token(blueprint, token, google_user_id) + + +def oauth_update_token(blueprint, token, provider_user_id): + session[blueprint.name + "_oauth_user_id"] = provider_user_id + session[blueprint.name + "_oauth_token"] = token + + # Find this OAuth token in the database, or create it + query = ub.session.query(ub.OAuth).filter_by( + provider=blueprint.name, + provider_user_id=provider_user_id, + ) + try: + oauth = query.one() + # update token + oauth.token = token + except NoResultFound: + oauth = ub.OAuth( + provider=blueprint.name, + provider_user_id=provider_user_id, + token=token, + ) + try: + ub.session.add(oauth) + ub.session.commit() + except Exception as e: + app.logger.exception(e) + ub.session.rollback() + + # Disable Flask-Dance's default behavior for saving the OAuth token + return False + + +def bind_oauth_or_register(provider, provider_user_id, redirect_url): + query = ub.session.query(ub.OAuth).filter_by( + provider=provider, + provider_user_id=provider_user_id, + ) + try: + oauth = query.one() + # already bind with user, just login + if oauth.user: + login_user(oauth.user) + return redirect(url_for('index')) + else: + # bind to current user + if current_user and not current_user.is_anonymous: + oauth.user = current_user + try: + ub.session.add(oauth) + ub.session.commit() + except Exception as e: + app.logger.exception(e) + ub.session.rollback() + return redirect(url_for('register')) + except NoResultFound: + return redirect(url_for(redirect_url)) + + +# notify on OAuth provider error +@oauth_error.connect_via(github_blueprint) +def github_error(blueprint, error, error_description=None, error_uri=None): + msg = ( + "OAuth error from {name}! " + "error={error} description={description} uri={uri}" + ).format( + name=blueprint.name, + error=error, + description=error_description, + uri=error_uri, + ) + flash(msg, category="error") + + +@app.route('/github') +@github_oauth_required +def github_login(): + if not github.authorized: + return redirect(url_for('github.login')) + account_info = github.get('/user') + if account_info.ok: + account_info_json = account_info.json() + return bind_oauth_or_register(github_blueprint.name, account_info_json['id'], 'github.login') + flash(_(u"GitHub Oauth error, please retry later."), category="error") + return redirect(url_for('login')) + + +@app.route('/google') +@google_oauth_required +def google_login(): + if not google.authorized: + return redirect(url_for("google.login")) + resp = google.get("/oauth2/v2/userinfo") + if resp.ok: + account_info_json = resp.json() + return bind_oauth_or_register(google_blueprint.name, account_info_json['id'], 'google.login') + flash(_(u"Google Oauth error, please retry later."), category="error") + return redirect(url_for('login')) + + +@oauth_error.connect_via(google_blueprint) +def google_error(blueprint, error, error_description=None, error_uri=None): + msg = ( + "OAuth error from {name}! " + "error={error} description={description} uri={uri}" + ).format( + name=blueprint.name, + error=error, + description=error_description, + uri=error_uri, + ) + flash(msg, category="error") diff --git a/requirements.txt b/requirements.txt index 3fb23ea3..2b13eb54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,5 @@ SQLAlchemy>=1.1.0 tornado>=4.1 Wand>=0.4.4 unidecode>=0.04.19 +flask-dance>=0.13.0 +sqlalchemy_utils>=0.33.5 From 4b76b8400da49f1d04e3f1eb67d0553346e7834f Mon Sep 17 00:00:00 2001 From: Jim Ma Date: Sat, 13 Oct 2018 14:40:08 +0800 Subject: [PATCH 005/349] Add OAuth link&unlink in user profile --- cps/templates/user_edit.html | 15 ++++++ cps/web.py | 96 ++++++++++++++++++++++++++++-------- 2 files changed, 90 insertions(+), 21 deletions(-) diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index ecf8042e..e5dcb59d 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -52,6 +52,21 @@ {% endfor %} + {% if registered_oauth.keys()| length > 0 %} +
+ +
+ {% for oauth, name in registered_oauth.iteritems() %} + + {% if oauth not in oauth_status %} + Link + {% else %} + Unlink + {% endif %} +
+ {% endfor %} +
+ {% endif %}
diff --git a/cps/web.py b/cps/web.py index 0937fdb7..a85bcf31 100644 --- a/cps/web.py +++ b/cps/web.py @@ -119,7 +119,7 @@ EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit' # EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + (['rar','cbr'] if rar_support else [])) -oauth_check = [] +oauth_check = {} '''class ReverseProxied(object): """Wrap the application in this middleware and configure the @@ -2751,6 +2751,7 @@ def profile(): downloads = list() languages = speaking_language() translations = babel.list_translations() + [LC('en')] + oauth_status = get_oauth_status() for book in content.downloads: downloadBook = db.session.query(db.Books).filter(db.Books.id == book.book_id).first() if downloadBook: @@ -2812,11 +2813,11 @@ def profile(): ub.session.rollback() flash(_(u"Found an existing account for this e-mail address."), category="error") return render_title_template("user_edit.html", content=content, downloads=downloads, - title=_(u"%(name)s's profile", name=current_user.nickname)) + title=_(u"%(name)s's profile", name=current_user.nickname, registered_oauth=oauth_check, oauth_status=oauth_status)) flash(_(u"Profile updated"), category="success") return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages, content=content, downloads=downloads, title=_(u"%(name)s's profile", - name=current_user.nickname), page="me") + name=current_user.nickname), page="me", registered_oauth=oauth_check, oauth_status=oauth_status) @app.route("/admin/view") @@ -3945,22 +3946,22 @@ def convert_bookformat(book_id): return redirect(request.environ["HTTP_REFERER"]) -def register_oauth_blueprint(blueprint): +def register_oauth_blueprint(blueprint, show_name): if blueprint.name != "": - oauth_check.append(blueprint.name) + oauth_check[blueprint.name] = show_name def register_user_with_oauth(user=None): - all_oauth = [] - for oauth in oauth_check: + all_oauth = {} + for oauth in oauth_check.keys(): if oauth + '_oauth_user_id' in session and session[oauth + '_oauth_user_id'] != '': - all_oauth.append(oauth) - if len(all_oauth) == 0: + all_oauth[oauth] = oauth_check[oauth] + if len(all_oauth.keys()) == 0: return if user is None: - flash(_(u"Register with %s" % ", ".join(all_oauth)), category="success") + flash(_(u"Register with %s" % ", ".join(list(all_oauth.values()))), category="success") else: - for oauth in all_oauth: + for oauth in all_oauth.keys(): # Find this OAuth token in the database, or create it query = ub.session.query(ub.OAuth).filter_by( provider=oauth, @@ -3980,7 +3981,7 @@ def register_user_with_oauth(user=None): def logout_oauth_user(): - for oauth in oauth_check: + for oauth in oauth_check.keys(): if oauth + '_oauth_user_id' in session: session.pop(oauth + '_oauth_user_id') @@ -4006,20 +4007,22 @@ app.register_blueprint(github_blueprint, url_prefix='/login') github_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) google_blueprint.backend = OAuthBackend(ub.OAuth, ub.session, user=current_user, user_required=True) -register_oauth_blueprint(github_blueprint) -register_oauth_blueprint(google_blueprint) + +if config.config_use_github_oauth: + register_oauth_blueprint(github_blueprint, 'GitHub') +if config.config_use_google_oauth: + register_oauth_blueprint(google_blueprint, 'Google') @oauth_authorized.connect_via(github_blueprint) def github_logged_in(blueprint, token): if not token: - flash("Failed to log in with GitHub.", category="error") + flash(_("Failed to log in with GitHub."), category="error") return False resp = blueprint.session.get("/user") if not resp.ok: - msg = "Failed to fetch user info from GitHub." - flash(msg, category="error") + flash(_("Failed to fetch user info from GitHub."), category="error") return False github_info = resp.json() @@ -4030,13 +4033,12 @@ def github_logged_in(blueprint, token): @oauth_authorized.connect_via(google_blueprint) def google_logged_in(blueprint, token): if not token: - flash("Failed to log in with Google.", category="error") + flash(_("Failed to log in with Google."), category="error") return False resp = blueprint.session.get("/oauth2/v2/userinfo") if not resp.ok: - msg = "Failed to fetch user info from Google." - flash(msg, category="error") + flash(_("Failed to fetch user info from Google."), category="error") return False google_info = resp.json() @@ -4088,7 +4090,7 @@ def bind_oauth_or_register(provider, provider_user_id, redirect_url): return redirect(url_for('index')) else: # bind to current user - if current_user and not current_user.is_anonymous: + if current_user and current_user.is_authenticated: oauth.user = current_user try: ub.session.add(oauth) @@ -4101,6 +4103,46 @@ def bind_oauth_or_register(provider, provider_user_id, redirect_url): return redirect(url_for(redirect_url)) +def get_oauth_status(): + status = [] + query = ub.session.query(ub.OAuth).filter_by( + user_id=current_user.id, + ) + try: + oauths = query.all() + for oauth in oauths: + status.append(oauth.provider) + return status + except NoResultFound: + return None + + +def unlink_oauth(provider): + if request.host_url + 'me' != request.referrer: + pass + query = ub.session.query(ub.OAuth).filter_by( + provider=provider, + user_id=current_user.id, + ) + try: + oauth = query.one() + if current_user and current_user.is_authenticated: + oauth.user = current_user + try: + ub.session.delete(oauth) + ub.session.commit() + logout_oauth_user() + flash(_("Unlink to %(oauth)s success.", oauth=oauth_check[provider]), category="success") + except Exception as e: + app.logger.exception(e) + ub.session.rollback() + flash(_("Unlink to %(oauth)s failed.", oauth=oauth_check[provider]), category="error") + except NoResultFound: + app.logger.warning("oauth %s for user %d not fount" % (provider, current_user.id)) + flash(_("Not linked to %(oauth)s.", oauth=oauth_check[provider]), category="error") + return redirect(url_for('profile')) + + # notify on OAuth provider error @oauth_error.connect_via(github_blueprint) def github_error(blueprint, error, error_description=None, error_uri=None): @@ -4129,6 +4171,12 @@ def github_login(): return redirect(url_for('login')) +@app.route('/unlink/github', methods=["GET"]) +@login_required +def github_login_unlink(): + return unlink_oauth(github_blueprint.name) + + @app.route('/google') @google_oauth_required def google_login(): @@ -4154,3 +4202,9 @@ def google_error(blueprint, error, error_description=None, error_uri=None): uri=error_uri, ) flash(msg, category="error") + + +@app.route('/unlink/google', methods=["GET"]) +@login_required +def google_login_unlink(): + return unlink_oauth(google_blueprint.name) From e1b6fa25e9cb80bc203245b3daac37215f2c0275 Mon Sep 17 00:00:00 2001 From: haseo Date: Mon, 19 Nov 2018 15:54:28 +0800 Subject: [PATCH 006/349] A better solution to #681 --- cps/web.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cps/web.py b/cps/web.py index 396066d9..5541ebcf 100644 --- a/cps/web.py +++ b/cps/web.py @@ -197,12 +197,7 @@ def get_locale(): if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings return user.locale translations = [item.language for item in babel.list_translations()] + ['en'] - preferred = [x.replace('-', '_') for x in request.accept_languages.values()] - - # In the case of Simplified Chinese, Accept-Language is "zh-CN", while our translation of Simplified Chinese is "zh_Hans_CN". - # TODO: This is Not a good solution, should be improved. - if "zh_CN" in preferred: - return "zh_Hans_CN" + preferred = [str(LC.parse(x, '-')) for x in request.accept_languages.values()] return negotiate_locale(preferred, translations) From 863b77a5d7d342ac2f587c6f8d70679c707189a6 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sun, 25 Nov 2018 11:25:20 +0100 Subject: [PATCH 007/349] Fix #711 Fixing for send to kindle after uploading codecleaning --- cps/helper.py | 25 +++++++-- cps/templates/detail.html | 8 ++- cps/web.py | 104 ++++++++++++++++++-------------------- 3 files changed, 73 insertions(+), 64 deletions(-) diff --git a/cps/helper.py b/cps/helper.py index 2834bad1..43fb8520 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -114,9 +114,9 @@ def send_registration_mail(e_mail, user_name, default_password, resend=False): return def check_send_to_kindle(entry): - ''' + """ returns all available book formats for sending to Kindle - ''' + """ if len(entry.data): bookformats=list() if ub.config.config_ebookconverter == 0: @@ -156,6 +156,18 @@ def check_send_to_kindle(entry): return None +# Check if a reader is existing for any of the book formats, if not, return empty list, otherwise return +# list with supported formats +def check_read_formats(entry): + EXTENSIONS_READER = {'TXT', 'PDF', 'EPUB', 'ZIP', 'CBZ', 'TAR', 'CBT', 'RAR', 'CBR'} + bookformats = list() + if len(entry.data): + for ele in iter(entry.data): + if ele.format in EXTENSIONS_READER: + bookformats.append(ele.format.lower()) + return bookformats + + # Files are processed in the following order/priority: # 1: If Mobi file is existing, it's directly send to kindle email, # 2: If Epub file is existing, it's converted and send to kindle email, @@ -336,6 +348,7 @@ def delete_book_gdrive(book, book_format): error =_(u'Book path %(path)s not found on Google Drive', path=book.path) # file not found return error + def generate_random_password(): s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%&*()?" passlen = 8 @@ -349,12 +362,14 @@ def update_dir_stucture(book_id, calibrepath): else: return update_dir_structure_file(book_id, calibrepath) + def delete_book(book, calibrepath, book_format): if ub.config.config_use_google_drive: return delete_book_gdrive(book, book_format) else: return delete_book_file(book, calibrepath, book_format) + def get_book_cover(cover_path): if ub.config.config_use_google_drive: try: @@ -372,6 +387,7 @@ def get_book_cover(cover_path): else: return send_from_directory(os.path.join(ub.config.config_calibre_dir, cover_path), "cover.jpg") + # saves book cover to gdrive or locally def save_cover(url, book_path): img = requests.get(url) @@ -384,7 +400,7 @@ def save_cover(url, book_path): f = open(os.path.join(tmpDir, "uploaded_cover.jpg"), "wb") f.write(img.content) f.close() - uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name)) + gd.uploadFileToEbooksFolder(os.path.join(book_path, 'cover.jpg'), os.path.join(tmpDir, f.name)) web.app.logger.info("Cover is saved on Google Drive") return True @@ -394,6 +410,7 @@ def save_cover(url, book_path): web.app.logger.info("Cover is saved") return True + def do_download_file(book, book_format, data, headers): if ub.config.config_use_google_drive: startTime = time.time() @@ -621,6 +638,7 @@ def get_current_version_info(): return {'hash': content[0], 'datetime': content[1]} return False + def json_serial(obj): """JSON serializer for objects not serializable by default json code""" @@ -628,6 +646,7 @@ def json_serial(obj): return obj.isoformat() raise TypeError ("Type %s not serializable" % type(obj)) + def render_task_status(tasklist): #helper function to apply localize status information in tasklist entries renderedtasklist=list() diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 27d73ae2..7631ce2f 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -57,17 +57,15 @@
{% endif %} {% endif %} - {% if entry.data|length %} + {% if reader_list %}
diff --git a/cps/web.py b/cps/web.py index 687a792c..558479cf 100644 --- a/cps/web.py +++ b/cps/web.py @@ -56,7 +56,6 @@ import tempfile from redirect import redirect_back import time import server -# import copy from reverseproxy import ReverseProxied try: @@ -116,8 +115,6 @@ EXTENSIONS_UPLOAD = {'txt', 'pdf', 'epub', 'mobi', 'azw', 'azw3', 'cbr', 'cbz', 'fb2', 'html', 'rtf', 'odt'} EXTENSIONS_CONVERT = {'pdf', 'epub', 'mobi', 'azw3', 'docx', 'rtf', 'fb2', 'lit', 'lrf', 'txt', 'html', 'rtf', 'odt'} -# EXTENSIONS_READER = set(['txt', 'pdf', 'epub', 'zip', 'cbz', 'tar', 'cbt'] + (['rar','cbr'] if rar_support else [])) - # Main code mimetypes.init() @@ -733,7 +730,8 @@ def feed_hot(): # ub.session.query(ub.Downloads).filter(book.Downloads.book_id == ub.Downloads.book_id).delete() # ub.session.commit() numBooks = entries.__len__() - pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, numBooks) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), + config.config_books_per_page, numBooks) return render_xml_template('feed.xml', entries=entries, pagination=pagination) @@ -773,7 +771,8 @@ def feed_publisherindex(): def feed_publisher(book_id): off = request.args.get("offset") or 0 entries, __, pagination = fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), - db.Books, db.Books.publishers.any(db.Publishers.id == book_id), [db.Books.timestamp.desc()]) + db.Books, db.Books.publishers.any(db.Publishers.id == book_id), + [db.Books.timestamp.desc()]) return render_xml_template('feed.xml', entries=entries, pagination=pagination) @@ -872,7 +871,8 @@ def get_opds_download_link(book_id, book_format): file_name = book.authors[0].name + '_' + file_name file_name = helper.get_valid_filename(file_name) headers = Headers() - headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf8')), book_format) + headers["Content-Disposition"] = "attachment; filename*=UTF-8''%s.%s" % (quote(file_name.encode('utf8')), + book_format) try: headers["Content-Type"] = mimetypes.types_map['.' + book_format] except KeyError: @@ -895,32 +895,8 @@ def get_metadata_calibre_companion(uuid): @app.route("/ajax/emailstat") @login_required def get_email_status_json(): - answer=list() - # UIanswer = list() tasks=helper.global_WorkerThread.get_taskstatus() - '''if not current_user.role_admin(): - for task in tasks: - if task['user'] == current_user.nickname: - if task['formStarttime']: - task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=get_locale()) - # task['formStarttime'] = "" - else: - if 'starttime' not in task: - task['starttime'] = "" - answer.append(task) - else: - for task in tasks: - if task['formStarttime']: - task['starttime'] = format_datetime(task['formStarttime'], format='short', locale=get_locale()) - task['formStarttime'] = "" - else: - if 'starttime' not in task: - task['starttime'] = "" - answer = tasks''' - - # UIanswer = copy.deepcopy(answer) answer = helper.render_task_status(tasks) - js=json.dumps(answer, default=helper.json_serial) response = make_response(js) response.headers["Content-Type"] = "application/json; charset=utf-8" @@ -1423,7 +1399,7 @@ def author_list(): @login_required_if_no_ano def author(book_id, page): entries, __, pagination = fill_indexpage(page, db.Books, db.Books.authors.any(db.Authors.id == book_id), - [db.Series.name, db.Books.series_index],db.books_series_link, db.Series) + [db.Series.name, db.Books.series_index],db.books_series_link, db.Series) if entries is None: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") return redirect(url_for("index")) @@ -1464,8 +1440,9 @@ def publisher_list(): def publisher(book_id, page): publisher = db.session.query(db.Publishers).filter(db.Publishers.id == book_id).first() if publisher: - entries, random, pagination = fill_indexpage(page, db.Books, db.Books.publishers.any(db.Publishers.id == book_id), - (db.Series.name, db.Books.series_index), db.books_series_link, db.Series) + entries, random, pagination = fill_indexpage(page, db.Books, + db.Books.publishers.any(db.Publishers.id == book_id), + (db.Series.name, db.Books.series_index), db.books_series_link, db.Series) return render_title_template('index.html', random=random, entries=entries, pagination=pagination, title=_(u"Publisher: %(name)s", name=publisher.name), page="publisher") else: @@ -1675,10 +1652,11 @@ def show_book(book_id): entries.tags = sort(entries.tags, key = lambda tag: tag.name) kindle_list = helper.check_send_to_kindle(entries) + reader_list = helper.check_read_formats(entries) return render_title_template('detail.html', entry=entries, cc=cc, is_xhr=request.is_xhr, title=entries.title, books_shelfs=book_in_shelfs, - have_read=have_read, kindle_list=kindle_list, page="book") + have_read=have_read, kindle_list=kindle_list, reader_list=reader_list, page="book") else: flash(_(u"Error opening eBook. File does not exist or file is not accessible:"), category="error") return redirect(url_for("index")) @@ -1795,7 +1773,8 @@ def delete_book(book_id, book_format): getattr(book, cc_string).remove(del_cc) db.session.delete(del_cc) else: - modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id], db.session, 'custom') + modify_database_object([u''], getattr(book, cc_string), db.cc_classes[c.id], + db.session, 'custom') db.session.query(db.Books).filter(db.Books.id == book_id).delete() else: db.session.query(db.Data).filter(db.Data.book == book.id).filter(db.Data.format == book_format).delete() @@ -1871,7 +1850,8 @@ def revoke_watch_gdrive(): last_watch_response = config.config_google_drive_watch_changes_response if last_watch_response: try: - gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'], last_watch_response['resourceId']) + gdriveutils.stopChannel(gdriveutils.Gdrive.Instance().drive, last_watch_response['id'], + last_watch_response['resourceId']) except HttpError: pass settings = ub.session.query(ub.Settings).first() @@ -2467,7 +2447,8 @@ def send_to_kindle(book_id, book_format, convert): if settings.get("mail_server", "mail.example.com") == "mail.example.com": flash(_(u"Please configure the SMTP mail settings first..."), category="error") elif current_user.kindle_mail: - result = helper.send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir, current_user.nickname) + result = helper.send_mail(book_id, book_format, convert, current_user.kindle_mail, config.config_calibre_dir, + current_user.nickname) if result is None: flash(_(u"Book successfully queued for sending to %(kindlemail)s", kindlemail=current_user.kindle_mail), category="success") @@ -2625,7 +2606,8 @@ def remove_from_shelf(shelf_id, book_id): else: app.logger.info("Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name) if not request.is_xhr: - flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), category="error") + flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name), + category="error") return redirect(url_for('index')) return "Sorry you are not allowed to remove a book from this shelf: %s" % shelf.name, 403 @@ -3111,9 +3093,9 @@ def configuration_helper(origin): content.config_rarfile_location = to_save["config_rarfile_location"].strip() else: flash(check[1], category="error") - return render_title_template("config_edit.html", content=config, origin=origin, gdrive=gdriveutils.gdrive_support, - goodreads=goodreads_support, rarfile_support=rar_support, - title=_(u"Basic Configuration")) + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdriveutils.gdrive_support, goodreads=goodreads_support, + rarfile_support=rar_support, title=_(u"Basic Configuration")) try: if content.config_use_google_drive and is_gdrive_ready() and not os.path.exists(config.config_calibre_dir + "/metadata.db"): gdriveutils.downloadFile(None, "metadata.db", config.config_calibre_dir + "/metadata.db") @@ -3128,15 +3110,17 @@ def configuration_helper(origin): logging.getLogger("book_formats").setLevel(config.config_log_level) except Exception as e: flash(e, category="error") - return render_title_template("config_edit.html", content=config, origin=origin, gdrive=gdriveutils.gdrive_support, - gdriveError=gdriveError, goodreads=goodreads_support, rarfile_support=rar_support, + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, + goodreads=goodreads_support, rarfile_support=rar_support, title=_(u"Basic Configuration"), page="config") if db_change: reload(db) if not db.setup_db(): flash(_(u'DB location is not valid, please enter correct path'), category="error") - return render_title_template("config_edit.html", content=config, origin=origin, gdrive=gdriveutils.gdrive_support, - gdriveError=gdriveError, goodreads=goodreads_support, rarfile_support=rar_support, + return render_title_template("config_edit.html", content=config, origin=origin, + gdrive=gdriveutils.gdrive_support,gdriveError=gdriveError, + goodreads=goodreads_support, rarfile_support=rar_support, title=_(u"Basic Configuration"), page="config") if reboot_required: # stop Server @@ -3150,8 +3134,9 @@ def configuration_helper(origin): else: gdrivefolders=list() return render_title_template("config_edit.html", origin=origin, success=success, content=config, - show_authenticate_google_drive=not is_gdrive_ready(), gdrive=gdriveutils.gdrive_support, - gdriveError=gdriveError, gdrivefolders=gdrivefolders, rarfile_support=rar_support, + show_authenticate_google_drive=not is_gdrive_ready(), + gdrive=gdriveutils.gdrive_support, gdriveError=gdriveError, + gdrivefolders=gdrivefolders, rarfile_support=rar_support, goodreads=goodreads_support, title=_(u"Basic Configuration"), page="config") @@ -3581,7 +3566,8 @@ def upload_single_file(request, book, book_id): return redirect(url_for('show_book', book_id=book.id)) file_size = os.path.getsize(saved_filename) - is_format = db.session.query(db.Data).filter(db.Data.book == book_id).filter(db.Data.format == file_ext.upper()).first() + is_format = db.session.query(db.Data).filter(db.Data.book == book_id).\ + filter(db.Data.format == file_ext.upper()).first() # Format entry already exists, no need to update the database if is_format: @@ -3611,7 +3597,8 @@ def upload_cover(request, book): try: os.makedirs(filepath) except OSError: - flash(_(u"Failed to create path for cover %(path)s (Permission denied).", cover=filepath), category="error") + flash(_(u"Failed to create path for cover %(path)s (Permission denied).", cover=filepath), + category="error") return redirect(url_for('show_book', book_id=book.id)) try: requested_file.save(saved_filename) @@ -3826,11 +3813,13 @@ def upload(): try: os.unlink(meta.file_path) except OSError: - flash(_(u"Failed to delete file %(file)s (Permission denied).", file= meta.file_path), category="warning") + flash(_(u"Failed to delete file %(file)s (Permission denied).", file= meta.file_path), + category="warning") if meta.cover is None: has_cover = 0 - copyfile(os.path.join(config.get_main_dir, "cps/static/generic_cover.jpg"), os.path.join(filepath, "cover.jpg")) + copyfile(os.path.join(config.get_main_dir, "cps/static/generic_cover.jpg"), + os.path.join(filepath, "cover.jpg")) else: has_cover = 1 move(meta.cover, os.path.join(filepath, "cover.jpg")) @@ -3896,8 +3885,7 @@ def upload(): # save data to database, reread data db.session.commit() db.session.connection().connection.connection.create_function("title_sort", 1, db.title_sort) - book = db.session.query(db.Books) \ - .filter(db.Books.id == book_id).filter(common_filters()).first() + book = db.session.query(db.Books).filter(db.Books.id == book_id).filter(common_filters()).first() # upload book to gdrive if nesseccary and add "(bookid)" to folder name if config.config_use_google_drive: @@ -3919,14 +3907,18 @@ def upload(): for author in db_book.authors: author_names.append(author.name) if len(request.files.getlist("btn-upload")) < 2: - cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns.datatype.notin_(db.cc_exceptions)).all() + cc = db.session.query(db.Custom_Columns).filter(db.Custom_Columns. + datatype.notin_(db.cc_exceptions)).all() if current_user.role_edit() or current_user.role_admin(): return render_title_template('book_edit.html', book=book, authors=author_names, cc=cc, title=_(u"edit metadata"), page="upload") book_in_shelfs = [] - flg_send_to_kindle = helper.chk_send_to_kindle(book_id) + kindle_list = helper.check_send_to_kindle(book) + reader_list = helper.check_read_formats(book) + return render_title_template('detail.html', entry=book, cc=cc, - title=book.title, books_shelfs=book_in_shelfs, flg_kindle=flg_send_to_kindle, page="upload") + title=book.title, books_shelfs=book_in_shelfs, kindle_list=kindle_list, + reader_list=reader_list, page="upload") return redirect(url_for("index")) From 8c93a7afdd7b1cc45d87af7a736f77448e09810a Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sun, 25 Nov 2018 12:48:53 +0100 Subject: [PATCH 008/349] Updated tests --- test/Calibre-Web TestSummary.html | 919 +++++++++++------------------- 1 file changed, 336 insertions(+), 583 deletions(-) diff --git a/test/Calibre-Web TestSummary.html b/test/Calibre-Web TestSummary.html index 85f133b6..e9503755 100644 --- a/test/Calibre-Web TestSummary.html +++ b/test/Calibre-Web TestSummary.html @@ -32,15 +32,15 @@
-

Start Time: 2018-10-28 21:05:48.274095

+

Start Time: 2018-11-25 12:06:17.948456

-

Stop Time: 2018-10-28 21:23:52.450214

+

Stop Time: 2018-11-25 12:24:09.768645

-

Duration: 0:18:04.176119

+

Duration: 0:17:51.820189

@@ -524,20 +524,20 @@ AssertionError: logfile config value is not empty after reseting to default - - test_ebook_convert.test_ebook_convert - 12 - 0 - 0 + + test_edit_books.test_edit_books + 22 + 3 + 2 0 - 12 + 17 - Detail + Detail -
test_SSL_smtp_setup_error
+
test_database_errors
@@ -561,7 +561,7 @@ AssertionError: logfile config value is not empty after reseting to default -
test_STARTTLS_smtp_setup_error
+
test_delete_book
@@ -585,7 +585,7 @@ AssertionError: logfile config value is not empty after reseting to default -
test_convert_deactivate
+
test_delete_format
@@ -607,57 +607,42 @@ AssertionError: logfile config value is not empty after reseting to default - + -
test_convert_email
+
test_edit_author
- SKIP + FAIL
-