diff --git a/cps.py b/cps.py index 4daa0d9b..7da3f790 100755 --- a/cps.py +++ b/cps.py @@ -41,8 +41,13 @@ from cps.shelf import shelf from cps.admin import admi from cps.gdrive import gdrive from cps.editbooks import editbook -from cps.kobo import kobo -from cps.kobo_auth import kobo_auth + +try: + from cps.kobo import kobo + from cps.kobo_auth import kobo_auth + kobo_available = True +except ImportError: + kobo_available = False try: from cps.oauth_bb import oauth @@ -61,8 +66,9 @@ def main(): app.register_blueprint(admi) app.register_blueprint(gdrive) app.register_blueprint(editbook) - app.register_blueprint(kobo) - app.register_blueprint(kobo_auth) + if kobo_available: + app.register_blueprint(kobo) + app.register_blueprint(kobo_auth) if oauth_available: app.register_blueprint(oauth) success = web_server.start() diff --git a/cps/about.py b/cps/about.py index aa1e866e..ceef7308 100644 --- a/cps/about.py +++ b/cps/about.py @@ -67,6 +67,7 @@ _VERSIONS = OrderedDict( Unidecode = unidecode_version, Flask_SimpleLDAP = u'installed' if bool(services.ldap) else u'not installed', Goodreads = u'installed' if bool(services.goodreads_support) else u'not installed', + jsonschema = services.SyncToken.__version__ if bool(services.SyncToken) else u'not installed', ) _VERSIONS.update(uploader.get_versions()) diff --git a/cps/admin.py b/cps/admin.py index b84f5a74..91ebe997 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -45,7 +45,8 @@ from .web import admin_required, render_title_template, before_request, unconfig feature_support = { 'ldap': False, # bool(services.ldap), - 'goodreads': bool(services.goodreads_support) + 'goodreads': bool(services.goodreads_support), + 'kobo': bool(services.kobo) } # try: @@ -63,6 +64,7 @@ except ImportError: oauth_check = {} + feature_support['gdrive'] = gdrive_support admi = Blueprint('admin', __name__) log = logger.create() @@ -568,7 +570,7 @@ def _configuration_update_helper(): # Remote login configuration _config_checkbox("config_remote_login") if not config.config_remote_login: - ub.session.query(ub.RemoteAuthToken).delete() + ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type==0).delete() # Goodreads configuration _config_checkbox("config_use_goodreads") @@ -693,7 +695,8 @@ def new_user(): if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: flash(_(u"Please fill out all fields!"), category="error") return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - registered_oauth=oauth_check, title=_(u"Add new user")) + registered_oauth=oauth_check, feature_support=feature_support, + title=_(u"Add new user")) content.password = generate_password_hash(to_save["password"]) existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower())\ .first() @@ -704,14 +707,15 @@ def new_user(): if config.config_public_reg and not check_valid_domain(to_save["email"]): flash(_(u"E-mail is not from valid domain"), category="error") return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - registered_oauth=oauth_check, title=_(u"Add new user")) + registered_oauth=oauth_check, feature_support=feature_support, + title=_(u"Add new user")) else: content.email = to_save["email"] else: flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, languages=languages, title=_(u"Add new user"), page="newuser", - registered_oauth=oauth_check) + feature_support=feature_support, registered_oauth=oauth_check) try: ub.session.add(content) ub.session.commit() @@ -729,7 +733,7 @@ def new_user(): # content.mature_content = bool(config.config_default_show & constants.MATURE_CONTENT) return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, languages=languages, title=_(u"Add new user"), page="newuser", - registered_oauth=oauth_check) + feature_support=feature_support, registered_oauth=oauth_check) @admi.route("/admin/mailsettings") @@ -850,8 +854,14 @@ def edit_user(user_id): content.kobo_user_key_hash = kobo_user_key_hash else: flash(_(u"Found an existing account for this Kobo UserKey."), category="error") - return render_title_template("user_edit.html", translations=translations, languages=languages, - new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check, + return render_title_template("user_edit.html", + translations=translations, + languages=languages, + new_user=0, + content=content, + downloads=downloads, + registered_oauth=oauth_check, + feature_support=feature_support, title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") if to_save["email"] and to_save["email"] != content.email: existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \ @@ -860,9 +870,15 @@ def edit_user(user_id): content.email = to_save["email"] else: flash(_(u"Found an existing account for this e-mail address."), category="error") - return render_title_template("user_edit.html", translations=translations, languages=languages, + return render_title_template("user_edit.html", + translations=translations, + languages=languages, mail_configured = config.get_mail_server_configured(), - new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check, + feature_support=feature_support, + new_user=0, + content=content, + downloads=downloads, + registered_oauth=oauth_check, title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") if "nickname" in to_save and to_save["nickname"] != content.nickname: # Query User nickname, if not existing, change @@ -877,6 +893,7 @@ def edit_user(user_id): new_user=0, content=content, downloads=downloads, registered_oauth=oauth_check, + feature_support=feature_support, title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") @@ -888,9 +905,15 @@ def edit_user(user_id): except IntegrityError: ub.session.rollback() flash(_(u"An unknown error occured."), category="error") - return render_title_template("user_edit.html", translations=translations, languages=languages, new_user=0, - content=content, downloads=downloads, registered_oauth=oauth_check, + return render_title_template("user_edit.html", + translations=translations, + languages=languages, + new_user=0, + content=content, + downloads=downloads, + registered_oauth=oauth_check, mail_configured=config.get_mail_server_configured(), + feature_support=feature_support, title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") diff --git a/cps/kobo.py b/cps/kobo.py index 67687008..73536551 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -17,10 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os import sys import uuid -from base64 import b64decode, b64encode from datetime import datetime from time import gmtime, strftime try: @@ -28,14 +26,12 @@ try: except ImportError: from urllib.parse import unquote -from jsonschema import validate, exceptions from flask import ( Blueprint, request, make_response, jsonify, json, - current_app, url_for, redirect, ) @@ -44,7 +40,7 @@ from werkzeug.datastructures import Headers from sqlalchemy import func import requests -from . import config, logger, kobo_auth, db, helper +from . import config, logger, kobo_auth, db, helper, services from .web import download_required KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB", "EPUB3"]} @@ -56,19 +52,6 @@ kobo_auth.register_url_value_preprocessor(kobo) log = logger.create() - -def b64encode_json(json_data): - if sys.version_info < (3, 0): - return b64encode(json.dumps(json_data)) - else: - return b64encode(json.dumps(json_data).encode()) - - -# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2. -def to_epoch_timestamp(datetime_object): - return (datetime_object - datetime(1970, 1, 1)).total_seconds() - - def get_store_url_for_current_request(): # Programmatically modify the current url to point to the official Kobo store base, sep, request_path_with_auth_token = request.full_path.rpartition("/kobo/") @@ -110,117 +93,11 @@ def redirect_or_proxy_request(): ) -class SyncToken: - """ The SyncToken is used to persist state accross requests. - When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service. - As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server. - - Attributes: - books_last_created: Datetime representing the newest book that the device knows about. - books_last_modified: Datetime representing the last modified book that the device knows about. - """ - - SYNC_TOKEN_HEADER = "x-kobo-synctoken" - VERSION = "1-0-0" - MIN_VERSION = "1-0-0" - - token_schema = { - "type": "object", - "properties": {"version": {"type": "string"}, "data": {"type": "object"},}, - } - # This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device. - # A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db. - data_schema_v1 = { - "type": "object", - "properties": { - "raw_kobo_store_token": {"type": "string"}, - "books_last_modified": {"type": "string"}, - "books_last_created": {"type": "string"}, - }, - } - - def __init__( - self, - raw_kobo_store_token="", - books_last_created=datetime.min, - books_last_modified=datetime.min, - ): - self.raw_kobo_store_token = raw_kobo_store_token - self.books_last_created = books_last_created - self.books_last_modified = books_last_modified - - @staticmethod - def from_headers(headers): - sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "") - if sync_token_header == "": - return SyncToken() - - # On the first sync from a Kobo device, we may receive the SyncToken - # from the official Kobo store. Without digging too deep into it, that - # token is of the form [b64encoded blob].[b64encoded blob 2] - if "." in sync_token_header: - return SyncToken(raw_kobo_store_token=sync_token_header) - - try: - sync_token_json = json.loads( - b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4)) - ) - validate(sync_token_json, SyncToken.token_schema) - if sync_token_json["version"] < SyncToken.MIN_VERSION: - raise ValueError - - data_json = sync_token_json["data"] - validate(sync_token_json, SyncToken.data_schema_v1) - except (exceptions.ValidationError, ValueError) as e: - log.error("Sync token contents do not follow the expected json schema.") - return SyncToken() - - raw_kobo_store_token = data_json["raw_kobo_store_token"] - try: - books_last_modified = datetime.utcfromtimestamp( - data_json["books_last_modified"] - ) - books_last_created = datetime.utcfromtimestamp( - data_json["books_last_created"] - ) - except TypeError: - log.error("SyncToken timestamps don't parse to a datetime.") - return SyncToken(raw_kobo_store_token=raw_kobo_store_token) - - return SyncToken( - raw_kobo_store_token=raw_kobo_store_token, - books_last_created=books_last_created, - books_last_modified=books_last_modified, - ) - - def set_kobo_store_header(self, store_headers): - store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token) - - def merge_from_store_response(self, store_response): - self.raw_kobo_store_token = store_response.headers.get( - SyncToken.SYNC_TOKEN_HEADER, "" - ) - - def to_headers(self, headers): - headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token() - - def build_sync_token(self): - token = { - "version": SyncToken.VERSION, - "data": { - "raw_kobo_store_token": self.raw_kobo_store_token, - "books_last_modified": to_epoch_timestamp(self.books_last_modified), - "books_last_created": to_epoch_timestamp(self.books_last_created), - }, - } - return b64encode_json(token) - - @kobo.route("/v1/library/sync") @login_required @download_required def HandleSyncRequest(): - sync_token = SyncToken.from_headers(request.headers) + sync_token = services.SyncToken.from_headers(request.headers) log.info("Kobo library sync request received.") # TODO: Limit the number of books return per sync call, and rely on the sync-continuatation header diff --git a/cps/kobo_auth.py b/cps/kobo_auth.py index 2077ce75..60f3ea5f 100644 --- a/cps/kobo_auth.py +++ b/cps/kobo_auth.py @@ -23,13 +23,13 @@ This module also includes research notes into the auth protocol used by Kobo dev Log-in: When first booting a Kobo device the user must sign into a Kobo (or affiliate) account. -Upon successful sign-in, the user is redirected to +Upon successful sign-in, the user is redirected to https://auth.kobobooks.com/CrossDomainSignIn?id= which serves the following response: . And triggers the insertion of a userKey into the device's User table. -Together, the device's DeviceId and UserKey act as an *irrevocable* authentication +Together, the device's DeviceId and UserKey act as an *irrevocable* authentication token to most (if not all) Kobo APIs. In fact, in most cases only the UserKey is required to authorize the API call. @@ -95,7 +95,7 @@ def load_user_from_kobo_request(request): user = ( ub.session.query(ub.User) .join(ub.RemoteAuthToken) - .filter(ub.RemoteAuthToken.auth_token == auth_token) + .filter(ub.RemoteAuthToken.auth_token == auth_token).filter(ub.RemoteAuthToken.token_type==1) .first() ) if user is not None: @@ -108,21 +108,23 @@ def load_user_from_kobo_request(request): kobo_auth = Blueprint("kobo_auth", __name__, url_prefix="/kobo_auth") -@kobo_auth.route("/generate_auth_token") +@kobo_auth.route("/generate_auth_token/") @login_required -def generate_auth_token(): +def generate_auth_token(user_id): # Invalidate any prevously generated Kobo Auth token for this user. - ub.session.query(ub.RemoteAuthToken).filter( - ub.RemoteAuthToken.user_id == current_user.id - ).delete() + auth_token = ub.session.query(ub.RemoteAuthToken).filter( + ub.RemoteAuthToken.user_id == user_id + ).filter(ub.RemoteAuthToken.token_type==1).first() - auth_token = ub.RemoteAuthToken() - auth_token.user_id = current_user.id - auth_token.expiration = datetime.max - auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8") + if not auth_token: + auth_token = ub.RemoteAuthToken() + auth_token.user_id = user_id + auth_token.expiration = datetime.max + auth_token.auth_token = (hexlify(urandom(16))).decode("utf-8") + auth_token.token_type = 1 - ub.session.add(auth_token) - ub.session.commit() + ub.session.add(auth_token) + ub.session.commit() return render_title_template( "generate_kobo_auth_url.html", @@ -131,3 +133,13 @@ def generate_auth_token(): "kobo.TopLevelEndpoint", auth_token=auth_token.auth_token, _external=True ), ) + + +@kobo_auth.route("/deleteauthtoken/") +@login_required +def delete_auth_token(user_id): + # Invalidate any prevously generated Kobo Auth token for this user. + ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.user_id == user_id)\ + .filter(ub.RemoteAuthToken.token_type==1).delete() + ub.session.commit() + return "" diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py new file mode 100644 index 00000000..2a17f7b5 --- /dev/null +++ b/cps/services/SyncToken.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 shavitmichael, OzzieIsaacs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +from base64 import b64decode, b64encode +from jsonschema import validate, exceptions, __version__ +from datetime import datetime +try: + from urllib import unquote +except ImportError: + from urllib.parse import unquote + +from flask import json +from .. import logger as log + + +def b64encode_json(json_data): + if sys.version_info < (3, 0): + return b64encode(json.dumps(json_data)) + else: + return b64encode(json.dumps(json_data).encode()) + + +# Python3 has a timestamp() method we could be calling, however it's not avaiable in python2. +def to_epoch_timestamp(datetime_object): + return (datetime_object - datetime(1970, 1, 1)).total_seconds() + + +class SyncToken: + """ The SyncToken is used to persist state accross requests. + When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service. + As an example use-case, the SyncToken is used to detect books that have been added to the library since the last time the device synced to the server. + + Attributes: + books_last_created: Datetime representing the newest book that the device knows about. + books_last_modified: Datetime representing the last modified book that the device knows about. + """ + + SYNC_TOKEN_HEADER = "x-kobo-synctoken" + VERSION = "1-0-0" + MIN_VERSION = "1-0-0" + + token_schema = { + "type": "object", + "properties": {"version": {"type": "string"}, "data": {"type": "object"},}, + } + # This Schema doesn't contain enough information to detect and propagate book deletions from Calibre to the device. + # A potential solution might be to keep a list of all known book uuids in the token, and look for any missing from the db. + data_schema_v1 = { + "type": "object", + "properties": { + "raw_kobo_store_token": {"type": "string"}, + "books_last_modified": {"type": "string"}, + "books_last_created": {"type": "string"}, + }, + } + + def __init__( + self, + raw_kobo_store_token="", + books_last_created=datetime.min, + books_last_modified=datetime.min, + ): + self.raw_kobo_store_token = raw_kobo_store_token + self.books_last_created = books_last_created + self.books_last_modified = books_last_modified + + @staticmethod + def from_headers(headers): + sync_token_header = headers.get(SyncToken.SYNC_TOKEN_HEADER, "") + if sync_token_header == "": + return SyncToken() + + # On the first sync from a Kobo device, we may receive the SyncToken + # from the official Kobo store. Without digging too deep into it, that + # token is of the form [b64encoded blob].[b64encoded blob 2] + if "." in sync_token_header: + return SyncToken(raw_kobo_store_token=sync_token_header) + + try: + sync_token_json = json.loads( + b64decode(sync_token_header + "=" * (-len(sync_token_header) % 4)) + ) + validate(sync_token_json, SyncToken.token_schema) + if sync_token_json["version"] < SyncToken.MIN_VERSION: + raise ValueError + + data_json = sync_token_json["data"] + validate(sync_token_json, SyncToken.data_schema_v1) + except (exceptions.ValidationError, ValueError) as e: + log.error("Sync token contents do not follow the expected json schema.") + return SyncToken() + + raw_kobo_store_token = data_json["raw_kobo_store_token"] + try: + books_last_modified = datetime.utcfromtimestamp( + data_json["books_last_modified"] + ) + books_last_created = datetime.utcfromtimestamp( + data_json["books_last_created"] + ) + except TypeError: + log.error("SyncToken timestamps don't parse to a datetime.") + return SyncToken(raw_kobo_store_token=raw_kobo_store_token) + + return SyncToken( + raw_kobo_store_token=raw_kobo_store_token, + books_last_created=books_last_created, + books_last_modified=books_last_modified, + ) + + def set_kobo_store_header(self, store_headers): + store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token) + + def merge_from_store_response(self, store_response): + self.raw_kobo_store_token = store_response.headers.get( + SyncToken.SYNC_TOKEN_HEADER, "" + ) + + def to_headers(self, headers): + headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token() + + def build_sync_token(self): + token = { + "version": SyncToken.VERSION, + "data": { + "raw_kobo_store_token": self.raw_kobo_store_token, + "books_last_modified": to_epoch_timestamp(self.books_last_modified), + "books_last_created": to_epoch_timestamp(self.books_last_created), + }, + } + return b64encode_json(token) diff --git a/cps/services/__init__.py b/cps/services/__init__.py index d468d0b7..18b49b88 100644 --- a/cps/services/__init__.py +++ b/cps/services/__init__.py @@ -35,4 +35,9 @@ except ImportError as err: log.debug("cannot import simpleldap, logging in with ldap will not work: %s", err) ldap = None - +try: + from . import SyncToken as SyncToken + kobo = True +except ImportError as err: + log.debug("cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err) + kobo = None diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 2b2716bf..8f54ba45 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -228,6 +228,41 @@ $(function() { $(this).find(".modal-body").html("..."); }); + $("#modal_kobo_token") + .on("show.bs.modal", function(e) { + var $modalBody = $(this).find(".modal-body"); + + // Prevent static assets from loading multiple times + var useCache = function(options) { + options.async = true; + options.cache = true; + }; + preFilters.add(useCache); + + $.get(e.relatedTarget.href).done(function(content) { + $modalBody.html(content); + preFilters.remove(useCache); + }); + }) + .on("hidden.bs.modal", function() { + $(this).find(".modal-body").html("..."); + $("#config_delete_kobo_token").show(); + }); + + $("#btndeletetoken").click(function() { + //get data-id attribute of the clicked element + var pathname = document.getElementsByTagName("script"), src = pathname[pathname.length-1].src; + var path = src.substring(0,src.lastIndexOf("/")); + // var domainId = $(this).value("domainId"); + $.ajax({ + method:"get", + url: path + "/../../kobo_auth/deleteauthtoken/" + this.value, + }); + $("#modalDeleteToken").modal("hide"); + $("#config_delete_kobo_token").hide(); + + }); + $(window).resize(function() { $(".discover .row").isotope("layout"); }); diff --git a/cps/templates/generate_kobo_auth_url.html b/cps/templates/generate_kobo_auth_url.html index 28b098cf..307b26bd 100644 --- a/cps/templates/generate_kobo_auth_url.html +++ b/cps/templates/generate_kobo_auth_url.html @@ -1,7 +1,6 @@ -{% extends "layout.html" %} +{% extends "fragment.html" %} {% block body %}
-

{{_('Generate Kobo Auth URL')}}

{{_('Open the .kobo/Kobo eReader.conf file in a text editor and add (or edit):')}}.

@@ -12,4 +11,4 @@ {{_('Please note that every visit to this current page invalidates any previously generated Authentication url for this user.')}}.

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/cps/templates/user_edit.html b/cps/templates/user_edit.html index 3e1963f1..a044f270 100644 --- a/cps/templates/user_edit.html +++ b/cps/templates/user_edit.html @@ -59,6 +59,13 @@ {% endfor %} {% endif %} + {% if feature_support['kobo'] %} + +
+ {{_('Create/View')}} + +
+ {% endif %}
{% for element in sidebar %} {% if element['config_show'] %} @@ -146,6 +153,35 @@
{% endif %} + + + + {% endblock %} {% block modal %} {{ restrict_modal() }} diff --git a/cps/ub.py b/cps/ub.py index c4c69500..e79c62b2 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -199,6 +199,7 @@ class User(UserBase, Base): allowed_tags = Column(String, default="") restricted_column_value = Column(String, default="") allowed_column_value = Column(String, default="") + remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic') if oauth_support: @@ -333,6 +334,7 @@ class RemoteAuthToken(Base): user_id = Column(Integer, ForeignKey('user.id')) verified = Column(Boolean, default=False) expiration = Column(DateTime) + token_type = Column(Integer, default=0) def __init__(self): self.auth_token = (hexlify(os.urandom(4))).decode('utf-8') @@ -364,6 +366,15 @@ def migrate_Database(session): conn.execute("ALTER TABLE registration ADD column 'allow' INTEGER") conn.execute("update registration set 'allow' = 1") session.commit() + try: + session.query(exists().where(RemoteAuthToken.token_type)).scalar() + session.commit() + except exc.OperationalError: # Database is not compatible, some columns are missing + conn = engine.connect() + conn.execute("ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0") + conn.execute("update remote_auth_token set 'token_type' = 0") + session.commit() + # Handle table exists, but no content cnt = session.query(Registration).count() if not cnt: diff --git a/cps/web.py b/cps/web.py index 243ca802..24f26953 100644 --- a/cps/web.py +++ b/cps/web.py @@ -55,7 +55,8 @@ from .redirect import redirect_back feature_support = { 'ldap': False, # bool(services.ldap), - 'goodreads': bool(services.goodreads_support) + 'goodreads': bool(services.goodreads_support), + 'kobo': bool(services.kobo) } try: @@ -1319,6 +1320,7 @@ def profile(): flash(_(u"E-mail is not from valid domain"), category="error") return render_title_template("user_edit.html", content=current_user, downloads=downloads, title=_(u"%(name)s's profile", name=current_user.nickname), page="me", + feature_support=feature_support, registered_oauth=oauth_check, oauth_status=oauth_status) if "nickname" in to_save and to_save["nickname"] != current_user.nickname: # Query User nickname, if not existing, change @@ -1329,6 +1331,7 @@ def profile(): return render_title_template("user_edit.html", translations=translations, languages=languages, + feature_support=feature_support, new_user=0, content=current_user, downloads=downloads, registered_oauth=oauth_check, @@ -1360,13 +1363,13 @@ def profile(): flash(_(u"Found an existing account for this e-mail address."), category="error") log.debug(u"Found an existing account for this e-mail address.") return render_title_template("user_edit.html", content=current_user, downloads=downloads, - translations=translations, + translations=translations, feature_support=feature_support, title=_(u"%(name)s's profile", name=current_user.nickname), page="me", registered_oauth=oauth_check, oauth_status=oauth_status) flash(_(u"Profile updated"), category="success") log.debug(u"Profile updated") return render_title_template("user_edit.html", translations=translations, profile=1, languages=languages, - content=current_user, downloads=downloads, + content=current_user, downloads=downloads, feature_support=feature_support, title= _(u"%(name)s's profile", name=current_user.nickname), page="me", registered_oauth=oauth_check, oauth_status=oauth_status) diff --git a/optional-requirements.txt b/optional-requirements.txt index 605667d2..84af8426 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -32,3 +32,6 @@ rarfile>=2.7 # other natsort>=2.2.0 git+https://github.com/OzzieIsaacs/comicapi.git@5346716578b2843f54d522f44d01bc8d25001d24#egg=comicapi + +#kobo integration +jsonschema>=3.2.0 diff --git a/requirements.txt b/requirements.txt index 2ef6835d..daf2538d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,3 @@ SQLAlchemy>=1.1.0 tornado>=4.1 Wand>=0.4.4 unidecode>=0.04.19 -jsonschema>=3.2.0