diff --git a/cps/__init__.py b/cps/__init__.py index c804d801..f6bb0cf7 100644 --- a/cps/__init__.py +++ b/cps/__init__.py @@ -127,7 +127,7 @@ def get_locale(): user = getattr(g, 'user', None) # user = None if user is not None and hasattr(user, "locale"): - if user.nickname != 'Guest': # if the account is the guest account bypass the config lang settings + if user.name != 'Guest': # if the account is the guest account bypass the config lang settings return user.locale preferred = list() diff --git a/cps/admin.py b/cps/admin.py index 3b2c237d..1d4b5a84 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -35,13 +35,15 @@ from flask import Blueprint, flash, redirect, url_for, abort, request, make_resp from flask_login import login_required, current_user, logout_user, confirm_login from flask_babel import gettext as _ from sqlalchemy import and_ +from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError from sqlalchemy.sql.expression import func, or_ from . import constants, logger, helper, services from .cli import filepicker from . import db, calibre_db, ub, web_server, get_locale, config, updater_thread, babel, gdriveutils -from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash +from .helper import check_valid_domain, send_test_mail, reset_password, generate_password_hash, check_email, \ + valid_email, check_username from .gdriveutils import is_gdrive_ready, gdrive_support from .render_template import render_title_template, get_sidebar_config from . import debug_info @@ -57,12 +59,12 @@ feature_support = { 'ldap': bool(services.ldap), 'goodreads': bool(services.goodreads_support), 'kobo': bool(services.kobo), - 'updater': constants.UPDATER_AVAILABLE + 'updater': constants.UPDATER_AVAILABLE, + 'gmail': bool(services.gmail) } try: - # pylint: disable=unused-import - import rarfile + import rarfile # pylint: disable=unused-import feature_support['rar'] = True except (ImportError, SyntaxError): feature_support['rar'] = False @@ -150,7 +152,7 @@ def shutdown(): else: showtext['text'] = _(u'Performing shutdown of server, please close window') # stop gevent/tornado server - web_server.stop(task == 0) + web_server.stop(task==0) return json.dumps(showtext) if task == 2: @@ -185,10 +187,10 @@ def admin(): else: commit = version['version'] - all_user = ub.session.query(ub.User).all() + allUser = ub.session.query(ub.User).all() email_settings = config.get_mail_settings() kobo_support = feature_support['kobo'] and config.config_kobo_sync - return render_title_template("admin.html", allUser=all_user, email=email_settings, config=config, commit=commit, + return render_title_template("admin.html", allUser=allUser, email=email_settings, config=config, commit=commit, feature_support=feature_support, kobo_support=kobo_support, title=_(u"Admin page"), page="admin") @@ -214,6 +216,173 @@ def view_configuration(): restrictColumns=restrict_columns, title=_(u"UI Configuration"), page="uiconfig") +@admi.route("/admin/usertable") +@login_required +@admin_required +def edit_user_table(): + visibility = current_user.view_settings.get('useredit', {}) + languages = calibre_db.speaking_language() + translations = babel.list_translations() + [LC('en')] + allUser = ub.session.query(ub.User) + if not config.config_anonbrowse: + allUser = allUser.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) + + return render_title_template("user_table.html", + users=allUser.all(), + translations=translations, + languages=languages, + visiblility=visibility, + all_roles=constants.ALL_ROLES, + sidebar_settings=constants.sidebar_settings, + title=_(u"Edit Users"), + page="usertable") + +@admi.route("/ajax/listusers") +@login_required +@admin_required +def list_users(): + off = request.args.get("offset") or 0 + limit = request.args.get("limit") or 10 + search = request.args.get("search") + all_user = ub.session.query(ub.User) + if not config.config_anonbrowse: + all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) + total_count = all_user.count() + if search: + users = all_user.filter(or_(func.lower(ub.User.name).ilike("%" + search + "%"), + func.lower(ub.User.kindle_mail).ilike("%" + search + "%"), + func.lower(ub.User.email).ilike("%" + search + "%")))\ + .offset(off).limit(limit).all() + filtered_count = len(users) + else: + users = all_user.offset(off).limit(limit).all() + filtered_count = total_count + + for user in users: + if user.default_language == "all": + user.default = _("all") + else: + user.default = LC.parse(user.default_language).get_language_name(get_locale()) + + table_entries = {'totalNotFiltered': total_count, 'total': filtered_count, "rows": users} + js_list = json.dumps(table_entries, cls=db.AlchemyEncoder) + response = make_response(js_list) + response.headers["Content-Type"] = "application/json; charset=utf-8" + return response + +@admi.route("/ajax/deleteuser") +@login_required +@admin_required +def delete_user(): + # ToDo User delete check also not last one + return "" + +@admi.route("/ajax/getlocale") +@login_required +@admin_required +def table_get_locale(): + locale = babel.list_translations() + [LC('en')] + ret = list() + current_locale = get_locale() + for loc in locale: + ret.append({'value': str(loc), 'text': loc.get_language_name(current_locale)}) + return json.dumps(ret) + + +@admi.route("/ajax/getdefaultlanguage") +@login_required +@admin_required +def table_get_default_lang(): + languages = calibre_db.speaking_language() + ret = list() + ret.append({'value':'all','text':_('Show All')}) + for lang in languages: + ret.append({'value': lang.lang_code, 'text': lang.name}) + return json.dumps(ret) + + +@admi.route("/ajax/editlistusers/", methods=['POST']) +@login_required +@admin_required +def edit_list_user(param): + vals = request.form.to_dict(flat=False) + all_user = ub.session.query(ub.User) + if not config.config_anonbrowse: + all_user = all_user.filter(ub.User.role.op('&')(constants.ROLE_ANONYMOUS) != constants.ROLE_ANONYMOUS) + # only one user is posted + if "pk" in vals: + users = [all_user.filter(ub.User.id == vals['pk'][0]).one_or_none()] + else: + if "pk[]" in vals: + users = all_user.filter(ub.User.id.in_(vals['pk[]'])).all() + else: + return "" + if 'field_index' in vals: + vals['field_index'] = vals['field_index'][0] + if 'value' in vals: + vals['value'] = vals['value'][0] + else: + return "" + for user in users: + try: + vals['value'] = vals['value'].strip() + if param == 'name': + if user.name == "Guest": + raise Exception(_("Guest Name can't be changed")) + user.name = check_username(vals['value']) + elif param =='email': + user.email = check_email(vals['value']) + elif param == 'kindle_mail': + user.kindle_mail = valid_email(vals['value']) if vals['value'] else "" + elif param == 'role': + if vals['value'] == 'true': + user.role |= int(vals['field_index']) + else: + if int(vals['field_index']) == constants.ROLE_ADMIN: + if not ub.session.query(ub.User).\ + filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, + ub.User.id != user.id).count(): + return _(u"No admin user remaining, can't remove admin role", nick=user.name), 400 + user.role &= ~int(vals['field_index']) + elif param == 'sidebar_view': + if vals['value'] == 'true': + user.sidebar_view |= int(vals['field_index']) + else: + user.sidebar_view &= ~int(vals['field_index']) + elif param == 'denied_tags': + user.denied_tags = vals['value'] + elif param == 'allowed_tags': + user.allowed_tags = vals['value'] + elif param == 'allowed_column_value': + user.allowed_column_value = vals['value'] + elif param == 'denied_column_value': + user.denied_column_value = vals['value'] + elif param == 'locale': + user.locale = vals['value'] + elif param == 'default_language': + user.default_language = vals['value'] + except Exception as ex: + return str(ex), 400 + ub.session_commit() + return "" + + +@admi.route("/ajax/user_table_settings", methods=['POST']) +@login_required +@admin_required +def update_table_settings(): + current_user.view_settings['useredit'] = json.loads(request.data) + try: + try: + flag_modified(current_user, "view_settings") + except AttributeError: + pass + ub.session.commit() + except (InvalidRequestError, OperationalError): + log.error("Invalid request received: {}".format(request)) + return "Invalid request", 400 + return "" + @admi.route("/admin/viewconfig", methods=["POST"]) @login_required @@ -262,6 +431,14 @@ def load_dialogtexts(element_id): texts["main"] = _('Do you really want to delete this user?') elif element_id == "delete_shelf": texts["main"] = _('Are you sure you want to delete this shelf?') + elif element_id == "select_locale": + texts["main"] = _('Are you sure you want to change locales of selected user(s)?') + elif element_id == "select_default_language": + texts["main"] = _('Are you sure you want to change visible book languages for selected user(s)?') + elif element_id == "role": + texts["main"] = _('Are you sure you want to change the selected role for the selected user(s)?') + elif element_id == "sidebar_view": + texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?') return json.dumps(texts) @@ -348,7 +525,7 @@ def edit_restriction(res_type, user_id): elementlist = usr.list_allowed_tags() elementlist[int(element['id'][1:])] = element['Element'] usr.allowed_tags = ','.join(elementlist) - ub.session_commit("Changed allowed tags of user {} to {}".format(usr.nickname, usr.allowed_tags)) + ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.allowed_tags)) if res_type == 3: # CColumn per user if isinstance(user_id, int): usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @@ -357,7 +534,7 @@ def edit_restriction(res_type, user_id): elementlist = usr.list_allowed_column_values() elementlist[int(element['id'][1:])] = element['Element'] usr.allowed_column_value = ','.join(elementlist) - ub.session_commit("Changed allowed columns of user {} to {}".format(usr.nickname, usr.allowed_column_value)) + ub.session_commit("Changed allowed columns of user {} to {}".format(usr.name, usr.allowed_column_value)) if element['id'].startswith('d'): if res_type == 0: # Tags as template elementlist = config.list_denied_tags() @@ -377,7 +554,7 @@ def edit_restriction(res_type, user_id): elementlist = usr.list_denied_tags() elementlist[int(element['id'][1:])] = element['Element'] usr.denied_tags = ','.join(elementlist) - ub.session_commit("Changed denied tags of user {} to {}".format(usr.nickname, usr.denied_tags)) + ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.denied_tags)) if res_type == 3: # CColumn per user if isinstance(user_id, int): usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @@ -386,7 +563,7 @@ def edit_restriction(res_type, user_id): elementlist = usr.list_denied_column_values() elementlist[int(element['id'][1:])] = element['Element'] usr.denied_column_value = ','.join(elementlist) - ub.session_commit("Changed denied columns of user {} to {}".format(usr.nickname, usr.denied_column_value)) + ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, usr.denied_column_value)) return "" @@ -433,10 +610,10 @@ def add_restriction(res_type, user_id): usr = current_user if 'submit_allow' in element: usr.allowed_tags = restriction_addition(element, usr.list_allowed_tags) - ub.session_commit("Changed allowed tags of user {} to {}".format(usr.nickname, usr.list_allowed_tags)) + ub.session_commit("Changed allowed tags of user {} to {}".format(usr.name, usr.list_allowed_tags)) elif 'submit_deny' in element: usr.denied_tags = restriction_addition(element, usr.list_denied_tags) - ub.session_commit("Changed denied tags of user {} to {}".format(usr.nickname, usr.list_denied_tags)) + ub.session_commit("Changed denied tags of user {} to {}".format(usr.name, usr.list_denied_tags)) if res_type == 3: # CustomC per user if isinstance(user_id, int): usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @@ -444,11 +621,11 @@ def add_restriction(res_type, user_id): usr = current_user if 'submit_allow' in element: usr.allowed_column_value = restriction_addition(element, usr.list_allowed_column_values) - ub.session_commit("Changed allowed columns of user {} to {}".format(usr.nickname, + ub.session_commit("Changed allowed columns of user {} to {}".format(usr.name, usr.list_allowed_column_values)) elif 'submit_deny' in element: usr.denied_column_value = restriction_addition(element, usr.list_denied_column_values) - ub.session_commit("Changed denied columns of user {} to {}".format(usr.nickname, + ub.session_commit("Changed denied columns of user {} to {}".format(usr.name, usr.list_denied_column_values)) return "" @@ -480,10 +657,10 @@ def delete_restriction(res_type, user_id): usr = current_user if element['id'].startswith('a'): usr.allowed_tags = restriction_deletion(element, usr.list_allowed_tags) - ub.session_commit("Deleted allowed tags of user {}: {}".format(usr.nickname, usr.list_allowed_tags)) + ub.session_commit("Deleted allowed tags of user {}: {}".format(usr.name, usr.list_allowed_tags)) elif element['id'].startswith('d'): usr.denied_tags = restriction_deletion(element, usr.list_denied_tags) - ub.session_commit("Deleted denied tags of user {}: {}".format(usr.nickname, usr.list_allowed_tags)) + ub.session_commit("Deleted denied tags of user {}: {}".format(usr.name, usr.list_allowed_tags)) elif res_type == 3: # Columns per user if isinstance(user_id, int): usr = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() @@ -491,12 +668,12 @@ def delete_restriction(res_type, user_id): usr = current_user if element['id'].startswith('a'): usr.allowed_column_value = restriction_deletion(element, usr.list_allowed_column_values) - ub.session_commit("Deleted allowed columns of user {}: {}".format(usr.nickname, + ub.session_commit("Deleted allowed columns of user {}: {}".format(usr.name, usr.list_allowed_column_values)) elif element['id'].startswith('d'): usr.denied_column_value = restriction_deletion(element, usr.list_denied_column_values) - ub.session_commit("Deleted denied columns of user {}: {}".format(usr.nickname, + ub.session_commit("Deleted denied columns of user {}: {}".format(usr.name, usr.list_denied_column_values)) return "" @@ -602,7 +779,6 @@ def pathchooser(): folders = [] files = [] - # locale = get_locale() for f in folders: try: data = {"name": f, "fullpath": os.path.join(cwd, f)} @@ -730,13 +906,35 @@ def _configuration_logfile_helper(to_save, gdrive_error): return reboot_required, None -def _configuration_ldap_check(reboot_required, to_save, gdrive_error): +def _configuration_ldap_helper(to_save, gdrive_error): + reboot_required = False + reboot_required |= _config_string(to_save, "config_ldap_provider_url") + reboot_required |= _config_int(to_save, "config_ldap_port") + reboot_required |= _config_int(to_save, "config_ldap_authentication") + reboot_required |= _config_string(to_save, "config_ldap_dn") + reboot_required |= _config_string(to_save, "config_ldap_serv_username") + reboot_required |= _config_string(to_save, "config_ldap_user_object") + reboot_required |= _config_string(to_save, "config_ldap_group_object_filter") + reboot_required |= _config_string(to_save, "config_ldap_group_members_field") + reboot_required |= _config_string(to_save, "config_ldap_member_user_object") + reboot_required |= _config_checkbox(to_save, "config_ldap_openldap") + reboot_required |= _config_int(to_save, "config_ldap_encryption") + reboot_required |= _config_string(to_save, "config_ldap_cacert_path") + reboot_required |= _config_string(to_save, "config_ldap_cert_path") + reboot_required |= _config_string(to_save, "config_ldap_key_path") + _config_string(to_save, "config_ldap_group_name") + if to_save.get("config_ldap_serv_password", "") != "": + reboot_required |= 1 + config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8') + config.save() + if not config.config_ldap_provider_url \ - or not config.config_ldap_port \ - or not config.config_ldap_dn \ - or not config.config_ldap_user_object: + or not config.config_ldap_port \ + or not config.config_ldap_dn \ + or not config.config_ldap_user_object: return reboot_required, _configuration_result(_('Please Enter a LDAP Provider, ' 'Port, DN and User Object Identifier'), gdrive_error) + if config.config_ldap_authentication > constants.LDAP_AUTH_ANONYMOUS: if config.config_ldap_authentication > constants.LDAP_AUTH_UNAUTHENTICATE: if not config.config_ldap_serv_username or not bool(config.config_ldap_serv_password): @@ -746,14 +944,6 @@ def _configuration_ldap_check(reboot_required, to_save, gdrive_error): if not config.config_ldap_serv_username: return reboot_required, _configuration_result('Please Enter a LDAP Service Account', gdrive_error) - if config.config_ldap_group_object_filter: - if config.config_ldap_group_object_filter.count("%s") != 1: - return reboot_required, \ - _configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'), - gdrive_error) - if config.config_ldap_group_object_filter.count("(") != config.config_ldap_group_object_filter.count(")"): - return reboot_required, _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'), - gdrive_error) if config.config_ldap_group_object_filter: if config.config_ldap_group_object_filter.count("%s") != 1: return reboot_required, \ @@ -771,7 +961,7 @@ def _configuration_ldap_check(reboot_required, to_save, gdrive_error): return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'), gdrive_error) - if "ldap_import_user_filter" in to_save and to_save["ldap_import_user_filter"] == '0': + if to_save.get("ldap_import_user_filter") == '0': config.config_ldap_member_user_object = "" else: if config.config_ldap_member_user_object.count("%s") != 1: @@ -793,31 +983,6 @@ def _configuration_ldap_check(reboot_required, to_save, gdrive_error): return reboot_required, None -def _configuration_ldap_helper(to_save, gdrive_error): - reboot_required = False - reboot_required |= _config_string(to_save, "config_ldap_provider_url") - reboot_required |= _config_int(to_save, "config_ldap_port") - reboot_required |= _config_int(to_save, "config_ldap_authentication") - reboot_required |= _config_string(to_save, "config_ldap_dn") - reboot_required |= _config_string(to_save, "config_ldap_serv_username") - reboot_required |= _config_string(to_save, "config_ldap_user_object") - reboot_required |= _config_string(to_save, "config_ldap_group_object_filter") - reboot_required |= _config_string(to_save, "config_ldap_group_members_field") - reboot_required |= _config_string(to_save, "config_ldap_member_user_object") - reboot_required |= _config_checkbox(to_save, "config_ldap_openldap") - reboot_required |= _config_int(to_save, "config_ldap_encryption") - reboot_required |= _config_string(to_save, "config_ldap_cacert_path") - reboot_required |= _config_string(to_save, "config_ldap_cert_path") - reboot_required |= _config_string(to_save, "config_ldap_key_path") - _config_string(to_save, "config_ldap_group_name") - if "config_ldap_serv_password" in to_save and to_save["config_ldap_serv_password"] != "": - reboot_required |= 1 - config.set_from_dictionary(to_save, "config_ldap_serv_password", base64.b64encode, encode='UTF-8') - config.save() - - return _configuration_ldap_check(reboot_required, to_save, gdrive_error) - - def _configuration_update_helper(configured): reboot_required = False db_change = False @@ -921,8 +1086,8 @@ def _configuration_update_helper(configured): if config.config_use_google_drive and is_gdrive_ready() and not os.path.exists(metadata_db): gdriveutils.downloadFile(None, "metadata.db", metadata_db) db_change = True - except Exception as e: - return _configuration_result('%s' % e, gdrive_error, configured) + except Exception as ex: + return _configuration_result('%s' % ex, gdrive_error, configured) if db_change: if not calibre_db.setup_db(config, ub.app_DB_path): @@ -974,7 +1139,6 @@ def _configuration_result(error_flash=None, gdrive_error=None, configured=True): def _handle_new_user(to_save, content, languages, translations, kobo_support): content.default_language = to_save["default_language"] - # content.mature_content = "Show_mature_content" in to_save content.locale = to_save.get("locale", content.locale) content.sidebar_view = sum(int(key[5:]) for key in to_save if key.startswith('show_')) @@ -982,28 +1146,21 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support): content.sidebar_view |= constants.DETAIL_RANDOM content.role = constants.selected_roles(to_save) - - if not to_save["nickname"] or not to_save["email"] or not to_save["password"]: - flash(_(u"Please fill out all fields!"), category="error") - return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, - registered_oauth=oauth_check, kobo_support=kobo_support, - title=_(u"Add new user")) content.password = generate_password_hash(to_save["password"]) - existing_user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == to_save["nickname"].lower()) \ - .first() - existing_email = ub.session.query(ub.User).filter(ub.User.email == to_save["email"].lower()) \ - .first() - if not existing_user and not existing_email: - content.nickname = to_save["nickname"] - 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, kobo_support=kobo_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") + try: + if not to_save["name"] or not to_save["email"] or not to_save["password"]: + log.info("Missing entries on new user") + raise Exception(_(u"Please fill out all fields!")) + content.email = check_email(to_save["email"]) + # Query User name, if not existing, change + content.name = check_username(to_save["name"]) + if to_save.get("kindle_mail"): + content.kindle_mail = valid_email(to_save["kindle_mail"]) + if config.config_public_reg and not check_valid_domain(content.email): + log.info("E-mail: {} for new user is not from valid domain".format(content.email)) + raise Exception(_(u"E-mail is not from valid domain")) + except Exception as ex: + flash(str(ex), category="error") return render_title_template("user_edit.html", new_user=1, content=content, translations=translations, languages=languages, title=_(u"Add new user"), page="newuser", kobo_support=kobo_support, registered_oauth=oauth_check) @@ -1014,49 +1171,33 @@ def _handle_new_user(to_save, content, languages, translations, kobo_support): content.denied_column_value = config.config_denied_column_value ub.session.add(content) ub.session.commit() - flash(_(u"User '%(user)s' created", user=content.nickname), category="success") + flash(_(u"User '%(user)s' created", user=content.name), category="success") return redirect(url_for('admin.admin')) except IntegrityError: ub.session.rollback() - flash(_(u"Found an existing account for this e-mail address or nickname."), category="error") - except OperationalError: - ub.session.rollback() - flash(_(u"Settings DB is not Writeable"), category="error") - -def delete_user(content): - if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, - ub.User.id != content.id).count(): - ub.session.query(ub.User).filter(ub.User.id == content.id).delete() - ub.session_commit() - flash(_(u"User '%(nick)s' deleted", nick=content.nickname), category="success") - return redirect(url_for('admin.admin')) - else: - flash(_(u"No admin user remaining, can't delete user", nick=content.nickname), category="error") - return redirect(url_for('admin.admin')) - - -def save_edited_user(content): - try: - ub.session_commit() - flash(_(u"User '%(nick)s' updated", nick=content.nickname), category="success") - except IntegrityError: - ub.session.rollback() - flash(_(u"An unknown error occured."), category="error") + flash(_(u"Found an existing account for this e-mail address or name."), category="error") except OperationalError: ub.session.rollback() flash(_(u"Settings DB is not Writeable"), category="error") def _handle_edit_user(to_save, content, languages, translations, kobo_support): - if "delete" in to_save: - return delete_user(content) + if to_save.get("delete"): + if ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, + ub.User.id != content.id).count(): + ub.session.query(ub.User).filter(ub.User.id == content.id).delete() + ub.session_commit() + flash(_(u"User '%(nick)s' deleted", nick=content.name), category="success") + return redirect(url_for('admin.admin')) + else: + flash(_(u"No admin user remaining, can't delete user", nick=content.name), category="error") + return redirect(url_for('admin.admin')) else: if not ub.session.query(ub.User).filter(ub.User.role.op('&')(constants.ROLE_ADMIN) == constants.ROLE_ADMIN, ub.User.id != content.id).count() and 'admin_role' not in to_save: - flash(_(u"No admin user remaining, can't remove admin role", nick=content.nickname), category="error") + flash(_(u"No admin user remaining, can't remove admin role", nick=content.name), category="error") return redirect(url_for('admin.admin')) - - if "password" in to_save and to_save["password"]: + if to_save.get("password"): content.password = generate_password_hash(to_save["password"]) anonymous = content.is_anonymous content.role = constants.selected_roles(to_save) @@ -1074,50 +1215,46 @@ def _handle_edit_user(to_save, content, languages, translations, kobo_support): elif value not in val and content.check_visibility(value): content.sidebar_view &= ~value - if "Show_detail_random" in to_save: + if to_save.get("Show_detail_random"): content.sidebar_view |= constants.DETAIL_RANDOM else: content.sidebar_view &= ~constants.DETAIL_RANDOM - if "default_language" in to_save: + if to_save.get("default_language"): content.default_language = to_save["default_language"] - if "locale" in to_save and to_save["locale"]: + if to_save.get("locale"): content.locale = to_save["locale"] - 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()) \ - .first() - if not existing_email: - 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, - mail_configured=config.get_mail_server_configured(), - kobo_support=kobo_support, - new_user=0, - content=content, - 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 - if not ub.session.query(ub.User).filter(ub.User.nickname == to_save["nickname"]).scalar(): - content.nickname = to_save["nickname"] - else: - flash(_(u"This username is already taken"), category="error") - return render_title_template("user_edit.html", - translations=translations, - languages=languages, - mail_configured=config.get_mail_server_configured(), - new_user=0, content=content, - registered_oauth=oauth_check, - kobo_support=kobo_support, - title=_(u"Edit User %(nick)s", nick=content.nickname), - page="edituser") - - if "kindle_mail" in to_save and to_save["kindle_mail"] != content.kindle_mail: - content.kindle_mail = to_save["kindle_mail"] - return save_edited_user(content) + try: + if to_save.get("email", content.email) != content.email: + content.email = check_email(to_save["email"]) + # Query User name, if not existing, change + if to_save.get("name", content.name) != content.name: + if to_save.get("name") == "Guest": + raise Exception(_("Guest Name can't be changed")) + content.name = check_username(to_save["name"]) + if to_save.get("kindle_mail") != content.kindle_mail: + content.kindle_mail = valid_email(to_save["kindle_mail"]) if to_save["kindle_mail"] else "" + except Exception as ex: + flash(str(ex), category="error") + return render_title_template("user_edit.html", + translations=translations, + languages=languages, + mail_configured=config.get_mail_server_configured(), + kobo_support=kobo_support, + new_user=0, + content=content, + registered_oauth=oauth_check, + title=_(u"Edit User %(nick)s", nick=content.name), + page="edituser") + try: + ub.session_commit() + flash(_(u"User '%(nick)s' updated", nick=content.name), category="success") + except IntegrityError: + ub.session.rollback() + flash(_(u"An unknown error occured."), category="error") + except OperationalError: + ub.session.rollback() + flash(_(u"Settings DB is not Writeable"), category="error") @admi.route("/admin/user/new", methods=["GET", "POST"]) @@ -1145,7 +1282,7 @@ def new_user(): def edit_mailsettings(): content = config.get_mail_settings() return render_title_template("email_edit.html", content=content, title=_(u"Edit E-mail Server Settings"), - page="mailset") + page="mailset", feature_support=feature_support) @admi.route("/admin/mailsettings", methods=["POST"]) @@ -1153,14 +1290,30 @@ def edit_mailsettings(): @admin_required def update_mailsettings(): to_save = request.form.to_dict() + _config_int(to_save, "mail_server_type") + if to_save.get("invalidate"): + config.mail_gmail_token = {} + try: + flag_modified(config, "mail_gmail_token") + except AttributeError: + pass + elif to_save.get("gmail"): + try: + config.mail_gmail_token = services.gmail.setup_gmail(config.mail_gmail_token) + flash(_(u"G-Mail Account Verification Successful"), category="success") + except Exception as ex: + flash(str(ex), category="error") + log.error(ex) + return edit_mailsettings() - _config_string(to_save, "mail_server") - _config_int(to_save, "mail_port") - _config_int(to_save, "mail_use_ssl") - _config_string(to_save, "mail_login") - _config_string(to_save, "mail_password") - _config_string(to_save, "mail_from") - _config_int(to_save, "mail_size", lambda y: int(y)*1024*1024) + else: + _config_string(to_save, "mail_server") + _config_int(to_save, "mail_port") + _config_int(to_save, "mail_use_ssl") + _config_string(to_save, "mail_login") + _config_string(to_save, "mail_password") + _config_string(to_save, "mail_from") + _config_int(to_save, "mail_size", lambda y: int(y)*1024*1024) try: config.save() except (OperationalError, InvalidRequestError): @@ -1170,10 +1323,10 @@ def update_mailsettings(): if to_save.get("test"): if current_user.email: - result = send_test_mail(current_user.email, current_user.nickname) + result = send_test_mail(current_user.email, current_user.name) if result is None: - flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result", email=current_user.email), - category="info") + flash(_(u"Test e-mail queued for sending to %(email)s, please check Tasks for result", + email=current_user.email), category="info") else: flash(_(u"There was an error sending the Test e-mail: %(res)s", res=result), category="error") else: @@ -1189,7 +1342,7 @@ def update_mailsettings(): @admin_required def edit_user(user_id): content = ub.session.query(ub.User).filter(ub.User.id == int(user_id)).first() # type: ub.User - if not content or (not config.config_anonbrowse and content.nickname == "Guest"): + if not content or (not config.config_anonbrowse and content.name == "Guest"): flash(_(u"User not found"), category="error") return redirect(url_for('admin.admin')) languages = calibre_db.speaking_language() @@ -1206,7 +1359,8 @@ def edit_user(user_id): registered_oauth=oauth_check, mail_configured=config.get_mail_server_configured(), kobo_support=kobo_support, - title=_(u"Edit User %(nick)s", nick=content.nickname), page="edituser") + title=_(u"Edit User %(nick)s", nick=content.name), + page="edituser") @admi.route("/admin/resetpassword/") @@ -1328,18 +1482,15 @@ def get_updater_status(): return '' -def create_ldap_user(user, user_data, config): - imported = 0 - showtext = None - +def ldap_import_create_user(user, user_data): user_login_field = extract_dynamic_field_from_filter(user, config.config_ldap_user_object) username = user_data[user_login_field][0].decode('utf-8') # check for duplicate username - if ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == username.lower()).first(): - # if ub.session.query(ub.User).filter(ub.User.nickname == username).first(): + if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).first(): + # if ub.session.query(ub.User).filter(ub.User.name == username).first(): log.warning("LDAP User %s Already in Database", user_data) - return imported, showtext + return 0, None kindlemail = '' if 'mail' in user_data: @@ -1350,13 +1501,15 @@ def create_ldap_user(user, user_data, config): else: log.debug('No Mail Field Found in LDAP Response') useremail = username + '@email.com' - # check for duplicate email - if ub.session.query(ub.User).filter(func.lower(ub.User.email) == useremail.lower()).first(): - log.warning("LDAP Email %s Already in Database", user_data) - return imported, showtext + try: + # check for duplicate email + useremail = check_email(useremail) + except Exception as ex: + log.warning("LDAP Email Error: {}, {}".format(user_data, ex)) + return 0, None content = ub.User() - content.nickname = username + content.name = username content.password = '' # dummy password which will be replaced by ldap one content.email = useremail content.kindle_mail = kindlemail @@ -1369,12 +1522,12 @@ def create_ldap_user(user, user_data, config): ub.session.add(content) try: ub.session.commit() - imported = 1 - except Exception as e: - log.warning("Failed to create LDAP user: %s - %s", user, e) + return 1, None # increase no of users + except Exception as ex: + log.warning("Failed to create LDAP user: %s - %s", user, ex) ub.session.rollback() - showtext = _(u'Failed to Create at Least One LDAP User') - return imported, showtext + message = _(u'Failed to Create at Least One LDAP User') + return 0, message @admi.route('/import_ldap_users') @@ -1404,23 +1557,23 @@ def import_ldap_users(): query_filter = config.config_ldap_user_object try: user_identifier = extract_user_identifier(user, query_filter) - except Exception as e: - log.warning(e) + except Exception as ex: + log.warning(ex) continue else: user_identifier = user query_filter = None try: user_data = services.ldap.get_object_details(user=user_identifier, query_filter=query_filter) - except AttributeError as e: - log.debug_or_exception(e) + except AttributeError as ex: + log.debug_or_exception(ex) continue if user_data: - success, txt = create_ldap_user(user, user_data, config) - # In case of error store text for showing it - if txt: - showtext['text'] = txt - imported += success + user_count, message = ldap_import_create_user(user, user_data) + if message: + showtext['text'] = message + else: + imported += user_count else: log.warning("LDAP User: %s Not Found", user) showtext['text'] = _(u'At Least One LDAP User Not Found in Database') diff --git a/cps/comic.py b/cps/comic.py index c1f1fd63..462c11f0 100644 --- a/cps/comic.py +++ b/cps/comic.py @@ -105,8 +105,8 @@ def _extract_Cover_from_archive(original_file_extension, tmp_file_name, rarExecu if extension in COVER_EXTENSIONS: cover_data = cf.read(name) break - except Exception as e: - log.debug('Rarfile failed with error: %s', e) + except Exception as ex: + log.debug('Rarfile failed with error: %s', ex) return cover_data diff --git a/cps/config_sql.py b/cps/config_sql.py index b80a6866..2ab0e3d6 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -23,7 +23,11 @@ import sys from sqlalchemy import exc, Column, String, Integer, SmallInteger, Boolean, BLOB, JSON from sqlalchemy.exc import OperationalError -from sqlalchemy.ext.declarative import declarative_base +try: + # Compability with sqlalchemy 2.0 + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from . import constants, cli, logger, ub @@ -35,7 +39,7 @@ class _Flask_Settings(_Base): __tablename__ = 'flask_settings' id = Column(Integer, primary_key=True) - flask_session_key = Column(BLOB, default="") + flask_session_key = Column(BLOB, default=b"") def __init__(self, key): self.flask_session_key = key @@ -54,6 +58,8 @@ class _Settings(_Base): mail_password = Column(String, default='mypassword') mail_from = Column(String, default='automailer ') mail_size = Column(Integer, default=25*1024*1024) + mail_server_type = Column(SmallInteger, default=0) + mail_gmail_token = Column(JSON, default={}) config_calibre_dir = Column(String) config_port = Column(Integer, default=constants.DEFAULT_PORT) @@ -242,15 +248,16 @@ class _ConfigSQL(object): return {k:v for k, v in self.__dict__.items() if k.startswith('mail_')} def get_mail_server_configured(self): - return not bool(self.mail_server == constants.DEFAULT_MAIL_SERVER) + return bool((self.mail_server != constants.DEFAULT_MAIL_SERVER and self.mail_server_type == 0) + or (self.mail_gmail_token != {} and self.mail_server_type == 1)) def set_from_dictionary(self, dictionary, field, convertor=None, default=None, encode=None): - '''Possibly updates a field of this object. + """Possibly updates a field of this object. The new value, if present, is grabbed from the given dictionary, and optionally passed through a convertor. :returns: `True` if the field has changed value - ''' + """ new_value = dictionary.get(field, default) if new_value is None: # log.debug("_ConfigSQL set_from_dictionary field '%s' not found", field) @@ -301,8 +308,11 @@ class _ConfigSQL(object): have_metadata_db = os.path.isfile(db_file) self.db_configured = have_metadata_db constants.EXTENSIONS_UPLOAD = [x.lstrip().rstrip().lower() for x in self.config_upload_formats.split(',')] - # pylint: disable=access-member-before-definition - logfile = logger.setup(self.config_logfile, self.config_log_level) + if os.environ.get('FLASK_DEBUG'): + logfile = logger.setup(logger.LOG_TO_STDOUT, logger.logging.DEBUG) + else: + # pylint: disable=access-member-before-definition + logfile = logger.setup(self.config_logfile, self.config_log_level) if logfile != self.config_logfile: log.warning("Log path %s not valid, falling back to default", self.config_logfile) self.config_logfile = logfile @@ -360,10 +370,14 @@ def _migrate_table(session, orm_class): if isinstance(column.default.arg, bool): column_default = ("DEFAULT %r" % int(column.default.arg)) else: - column_default = ("DEFAULT %r" % column.default.arg) + column_default = ("DEFAULT '%r'" % column.default.arg) + if isinstance(column.type, JSON): + column_type = "JSON" + else: + column_type = column.type alter_table = "ALTER TABLE %s ADD COLUMN `%s` %s %s" % (orm_class.__tablename__, column_name, - column.type, + column_type, column_default) log.debug(alter_table) session.execute(alter_table) @@ -426,12 +440,12 @@ def load_configuration(session): session.commit() conf = _ConfigSQL(session) # Migrate from global restrictions to user based restrictions - if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "": - conf.config_denied_tags = conf.config_mature_content_tags - conf.save() - session.query(ub.User).filter(ub.User.mature_content != True). \ - update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False) - session.commit() + #if bool(conf.config_default_show & constants.MATURE_CONTENT) and conf.config_denied_tags == "": + # conf.config_denied_tags = conf.config_mature_content_tags + # conf.save() + # session.query(ub.User).filter(ub.User.mature_content != True). \ + # update({"denied_tags": conf.config_mature_content_tags}, synchronize_session=False) + # session.commit() return conf def get_flask_session_key(session): diff --git a/cps/constants.py b/cps/constants.py index 2c00d434..e9c26cb1 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -21,8 +21,10 @@ import sys import os from collections import namedtuple -# if installed via pip this variable is set to true -HOME_CONFIG = False +# if installed via pip this variable is set to true (empty file with name .HOMEDIR present) +HOME_CONFIG = os.path.isfile(os.path.join(os.path.dirname(os.path.abspath(__file__)), '.HOMEDIR')) + +#In executables updater is not available, so variable is set to False there UPDATER_AVAILABLE = True # Base dir is parent of current file, necessary if called from different folder @@ -86,6 +88,26 @@ SIDEBAR_ARCHIVED = 1 << 15 SIDEBAR_DOWNLOAD = 1 << 16 SIDEBAR_LIST = 1 << 17 +sidebar_settings = { + "detail_random": DETAIL_RANDOM, + "sidebar_language": SIDEBAR_LANGUAGE, + "sidebar_series": SIDEBAR_SERIES, + "sidebar_category": SIDEBAR_CATEGORY, + "sidebar_random": SIDEBAR_RANDOM, + "sidebar_author": SIDEBAR_AUTHOR, + "sidebar_best_rated": SIDEBAR_BEST_RATED, + "sidebar_read_and_unread": SIDEBAR_READ_AND_UNREAD, + "sidebar_recent": SIDEBAR_RECENT, + "sidebar_sorted": SIDEBAR_SORTED, + "sidebar_publisher": SIDEBAR_PUBLISHER, + "sidebar_rating": SIDEBAR_RATING, + "sidebar_format": SIDEBAR_FORMAT, + "sidebar_archived": SIDEBAR_ARCHIVED, + "sidebar_download": SIDEBAR_DOWNLOAD, + "sidebar_list": SIDEBAR_LIST, + } + + ADMIN_USER_ROLES = sum(r for r in ALL_ROLES.values()) & ~ROLE_ANONYMOUS ADMIN_USER_SIDEBAR = (SIDEBAR_LIST << 1) - 1 diff --git a/cps/db.py b/cps/db.py index 3eec6454..5cb04ed3 100644 --- a/cps/db.py +++ b/cps/db.py @@ -30,7 +30,13 @@ from sqlalchemy import Table, Column, ForeignKey, CheckConstraint from sqlalchemy import String, Integer, Boolean, TIMESTAMP, Float from sqlalchemy.orm import relationship, sessionmaker, scoped_session from sqlalchemy.orm.collections import InstrumentedList -from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta +from sqlalchemy.ext.declarative import DeclarativeMeta +from sqlalchemy.exc import OperationalError +try: + # Compability with sqlalchemy 2.0 + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.pool import StaticPool from sqlalchemy.sql.expression import and_, true, false, text, func, or_ from sqlalchemy.ext.associationproxy import association_proxy @@ -326,7 +332,6 @@ class Books(Base): has_cover = Column(Integer, default=0) uuid = Column(String) isbn = Column(String(collation='NOCASE'), default="") - # Iccn = Column(String(collation='NOCASE'), default="") flags = Column(Integer, nullable=False, default=1) authors = relationship('Authors', secondary=books_authors_link, backref='books') @@ -437,12 +442,80 @@ class CalibreDB(): self.instances.add(self) - def initSession(self, expire_on_commit=True): self.session = self.session_factory() self.session.expire_on_commit = expire_on_commit self.update_title_sort(self.config) + @classmethod + def setup_db_cc_classes(self, cc): + cc_ids = [] + books_custom_column_links = {} + for row in cc: + if row.datatype not in cc_exceptions: + if row.datatype == 'series': + dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link', + 'id': Column(Integer, primary_key=True), + 'book': Column(Integer, ForeignKey('books.id'), + primary_key=True), + 'map_value': Column('value', Integer, + ForeignKey('custom_column_' + + str(row.id) + '.id'), + primary_key=True), + 'extra': Column(Float), + 'asoc': relationship('custom_column_' + str(row.id), uselist=False), + 'value': association_proxy('asoc', 'value') + } + books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'), + (Base,), dicttable) + else: + books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', + Base.metadata, + Column('book', Integer, ForeignKey('books.id'), + primary_key=True), + Column('value', Integer, + ForeignKey('custom_column_' + + str(row.id) + '.id'), + primary_key=True) + ) + cc_ids.append([row.id, row.datatype]) + + ccdict = {'__tablename__': 'custom_column_' + str(row.id), + 'id': Column(Integer, primary_key=True)} + if row.datatype == 'float': + ccdict['value'] = Column(Float) + elif row.datatype == 'int': + ccdict['value'] = Column(Integer) + elif row.datatype == 'bool': + ccdict['value'] = Column(Boolean) + else: + ccdict['value'] = Column(String) + if row.datatype in ['float', 'int', 'bool']: + ccdict['book'] = Column(Integer, ForeignKey('books.id')) + cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict) + + for cc_id in cc_ids: + if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'): + setattr(Books, + 'custom_column_' + str(cc_id[0]), + relationship(cc_classes[cc_id[0]], + primaryjoin=( + Books.id == cc_classes[cc_id[0]].book), + backref='books')) + elif (cc_id[1] == 'series'): + setattr(Books, + 'custom_column_' + str(cc_id[0]), + relationship(books_custom_column_links[cc_id[0]], + backref='books')) + else: + setattr(Books, + 'custom_column_' + str(cc_id[0]), + relationship(cc_classes[cc_id[0]], + secondary=books_custom_column_links[cc_id[0]], + backref='books')) + + return cc_classes + @classmethod def setup_db(cls, config, app_db_path): cls.config = config @@ -465,84 +538,24 @@ class CalibreDB(): isolation_level="SERIALIZABLE", connect_args={'check_same_thread': False}, poolclass=StaticPool) - cls.engine.execute("attach database '{}' as calibre;".format(dbpath)) - cls.engine.execute("attach database '{}' as app_settings;".format(app_db_path)) + with cls.engine.begin() as connection: + connection.execute(text("attach database '{}' as calibre;".format(dbpath))) + connection.execute(text("attach database '{}' as app_settings;".format(app_db_path))) conn = cls.engine.connect() # conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302 - except Exception as e: - config.invalidate(e) + except Exception as ex: + config.invalidate(ex) return False config.db_configured = True if not cc_classes: - cc = conn.execute("SELECT id, datatype FROM custom_columns") - - cc_ids = [] - books_custom_column_links = {} - for row in cc: - if row.datatype not in cc_exceptions: - if row.datatype == 'series': - dicttable = {'__tablename__': 'books_custom_column_' + str(row.id) + '_link', - 'id': Column(Integer, primary_key=True), - 'book': Column(Integer, ForeignKey('books.id'), - primary_key=True), - 'map_value': Column('value', Integer, - ForeignKey('custom_column_' + - str(row.id) + '.id'), - primary_key=True), - 'extra': Column(Float), - 'asoc': relationship('custom_column_' + str(row.id), uselist=False), - 'value': association_proxy('asoc', 'value') - } - books_custom_column_links[row.id] = type(str('books_custom_column_' + str(row.id) + '_link'), - (Base,), dicttable) - else: - books_custom_column_links[row.id] = Table('books_custom_column_' + str(row.id) + '_link', - Base.metadata, - Column('book', Integer, ForeignKey('books.id'), - primary_key=True), - Column('value', Integer, - ForeignKey('custom_column_' + - str(row.id) + '.id'), - primary_key=True) - ) - cc_ids.append([row.id, row.datatype]) - - ccdict = {'__tablename__': 'custom_column_' + str(row.id), - 'id': Column(Integer, primary_key=True)} - if row.datatype == 'float': - ccdict['value'] = Column(Float) - elif row.datatype == 'int': - ccdict['value'] = Column(Integer) - elif row.datatype == 'bool': - ccdict['value'] = Column(Boolean) - else: - ccdict['value'] = Column(String) - if row.datatype in ['float', 'int', 'bool']: - ccdict['book'] = Column(Integer, ForeignKey('books.id')) - cc_classes[row.id] = type(str('custom_column_' + str(row.id)), (Base,), ccdict) - - for cc_id in cc_ids: - if (cc_id[1] == 'bool') or (cc_id[1] == 'int') or (cc_id[1] == 'float'): - setattr(Books, - 'custom_column_' + str(cc_id[0]), - relationship(cc_classes[cc_id[0]], - primaryjoin=( - Books.id == cc_classes[cc_id[0]].book), - backref='books')) - elif (cc_id[1] == 'series'): - setattr(Books, - 'custom_column_' + str(cc_id[0]), - relationship(books_custom_column_links[cc_id[0]], - backref='books')) - else: - setattr(Books, - 'custom_column_' + str(cc_id[0]), - relationship(cc_classes[cc_id[0]], - secondary=books_custom_column_links[cc_id[0]], - backref='books')) + try: + cc = conn.execute("SELECT id, datatype FROM custom_columns") + cls.setup_db_cc_classes(cc) + except OperationalError as e: + log.debug_or_exception(e) cls.session_factory = scoped_session(sessionmaker(autocommit=False, autoflush=True, @@ -614,13 +627,18 @@ class CalibreDB(): randm = self.session.query(Books) \ .filter(self.common_filters(allow_show_archived)) \ .order_by(func.random()) \ - .limit(self.config.config_random_books) + .limit(self.config.config_random_books).all() else: randm = false() off = int(int(pagesize) * (page - 1)) - query = self.session.query(database) \ - .join(*join, isouter=True) \ - .filter(db_filter) \ + query = self.session.query(database) + if len(join) == 3: + query = query.outerjoin(join[0], join[1]).outerjoin(join[2]) + elif len(join) == 2: + query = query.outerjoin(join[0], join[1]) + elif len(join) == 1: + query = query.outerjoin(join[0]) + query = query.filter(db_filter)\ .filter(self.common_filters(allow_show_archived)) entries = list() pagination = list() @@ -628,8 +646,8 @@ class CalibreDB(): pagination = Pagination(page, pagesize, len(query.all())) entries = query.order_by(*order).offset(off).limit(pagesize).all() - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) #for book in entries: # book = self.order_authors(book) return entries, randm, pagination @@ -773,7 +791,7 @@ class CalibreDB(): def lcase(s): try: return unidecode.unidecode(s.lower()) - except Exception as e: + except Exception as ex: log = logger.create() - log.debug_or_exception(e) + log.debug_or_exception(ex) return s.lower() diff --git a/cps/editbooks.py b/cps/editbooks.py index e4265a20..cb8388ef 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -27,6 +27,8 @@ import json from shutil import copyfile from uuid import uuid4 +from babel import Locale as LC +from babel.core import UnknownLocaleError from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response from flask_babel import gettext as _ from flask_login import current_user, login_required @@ -68,17 +70,7 @@ def edit_required(f): return inner - -# Modifies different Database objects, first check if elements have to be added to database, than check -# if elements have to be deleted, because they are no longer used -def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type): - # passing input_elements not as a list may lead to undesired results - if not isinstance(input_elements, list): - raise TypeError(str(input_elements) + " should be passed as a list") - changed = False - input_elements = [x for x in input_elements if x != ''] - # we have all input element (authors, series, tags) names now - # 1. search for elements to remove +def search_objects_remove(db_book_object, db_type, input_elements): del_elements = [] for c_elements in db_book_object: found = False @@ -96,7 +88,10 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session # if the element was not found in the new list, add it to remove list if not found: del_elements.append(c_elements) - # 2. search for elements that need to be added + return del_elements + + +def search_objects_add(db_book_object, db_type, input_elements): add_elements = [] for inp_element in input_elements: found = False @@ -112,64 +107,96 @@ def modify_database_object(input_elements, db_book_object, db_object, db_session break if not found: add_elements.append(inp_element) - # if there are elements to remove, we remove them now + return add_elements + + +def remove_objects(db_book_object, db_session, del_elements): + changed = False if len(del_elements) > 0: for del_element in del_elements: db_book_object.remove(del_element) changed = True if len(del_element.books) == 0: db_session.delete(del_element) + return changed + +def add_objects(db_book_object, db_object, db_session, db_type, add_elements): + changed = False + if db_type == 'languages': + db_filter = db_object.lang_code + elif db_type == 'custom': + db_filter = db_object.value + else: + db_filter = db_object.name + for add_element in add_elements: + # check if a element with that name exists + db_element = db_session.query(db_object).filter(db_filter == add_element).first() + # if no element is found add it + # if new_element is None: + if db_type == 'author': + new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "") + elif db_type == 'series': + new_element = db_object(add_element, add_element) + elif db_type == 'custom': + new_element = db_object(value=add_element) + elif db_type == 'publisher': + new_element = db_object(add_element, None) + else: # db_type should be tag or language + new_element = db_object(add_element) + if db_element is None: + changed = True + db_session.add(new_element) + db_book_object.append(new_element) + else: + db_element = create_objects_for_addition(db_element, add_element, db_type) + changed = True + # add element to book + changed = True + db_book_object.append(db_element) + return changed + + +def create_objects_for_addition(db_element, add_element, db_type): + if db_type == 'custom': + if db_element.value != add_element: + db_element.value = add_element # ToDo: Before new_element, but this is not plausible + elif db_type == 'languages': + if db_element.lang_code != add_element: + db_element.lang_code = add_element + elif db_type == 'series': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = add_element + elif db_type == 'author': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = add_element.replace('|', ',') + elif db_type == 'publisher': + if db_element.name != add_element: + db_element.name = add_element + db_element.sort = None + elif db_element.name != add_element: + db_element.name = add_element + return db_element + + +# Modifies different Database objects, first check if elements if elements have to be deleted, +# because they are no longer used, than check if elements have to be added to database +def modify_database_object(input_elements, db_book_object, db_object, db_session, db_type): + # passing input_elements not as a list may lead to undesired results + if not isinstance(input_elements, list): + raise TypeError(str(input_elements) + " should be passed as a list") + input_elements = [x for x in input_elements if x != ''] + # we have all input element (authors, series, tags) names now + # 1. search for elements to remove + del_elements = search_objects_remove(db_book_object, db_type, input_elements) + # 2. search for elements that need to be added + add_elements = search_objects_add(db_book_object, db_type, input_elements) + # if there are elements to remove, we remove them now + changed = remove_objects(db_book_object, db_session, del_elements) # if there are elements to add, we add them now! if len(add_elements) > 0: - if db_type == 'languages': - db_filter = db_object.lang_code - elif db_type == 'custom': - db_filter = db_object.value - else: - db_filter = db_object.name - for add_element in add_elements: - # check if a element with that name exists - db_element = db_session.query(db_object).filter(db_filter == add_element).first() - # if no element is found add it - # if new_element is None: - if db_type == 'author': - new_element = db_object(add_element, helper.get_sorted_author(add_element.replace('|', ',')), "") - elif db_type == 'series': - new_element = db_object(add_element, add_element) - elif db_type == 'custom': - new_element = db_object(value=add_element) - elif db_type == 'publisher': - new_element = db_object(add_element, None) - else: # db_type should be tag or language - new_element = db_object(add_element) - if db_element is None: - changed = True - db_session.add(new_element) - db_book_object.append(new_element) - else: - if db_type == 'custom': - if db_element.value != add_element: - new_element.value = add_element - elif db_type == 'languages': - if db_element.lang_code != add_element: - db_element.lang_code = add_element - elif db_type == 'series': - if db_element.name != add_element: - db_element.name = add_element - db_element.sort = add_element - elif db_type == 'author': - if db_element.name != add_element: - db_element.name = add_element - db_element.sort = add_element.replace('|', ',') - elif db_type == 'publisher': - if db_element.name != add_element: - db_element.name = add_element - db_element.sort = None - elif db_element.name != add_element: - db_element.name = add_element - # add element to book - changed = True - db_book_object.append(db_element) + changed |= add_objects(db_book_object, db_object, db_session, db_type, add_elements) return changed @@ -309,8 +336,8 @@ def delete_book(book_id, book_format, jsonResponse): calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\ filter(db.Data.format == book_format).delete() calibre_db.session.commit() - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) calibre_db.session.rollback() else: # book not found @@ -422,7 +449,7 @@ def edit_book_comments(comments, book): return modif_date -def edit_book_languages(languages, book, upload=False): +def edit_book_languages(languages, book, upload=False, invalid=None): input_languages = languages.split(',') unknown_languages = [] if not upload: @@ -431,7 +458,10 @@ def edit_book_languages(languages, book, upload=False): input_l = isoLanguages.get_valid_language_codes(get_locale(), input_languages, unknown_languages) for l in unknown_languages: log.error('%s is not a valid language', l) - flash(_(u"%(langname)s is not a valid language", langname=l), category="warning") + if isinstance(invalid, list): + invalid.append(l) + else: + flash(_(u"%(langname)s is not a valid language", langname=l), category="warning") # ToDo: Not working correct if upload and len(input_l) == 1: # If the language of the file is excluded from the users view, it's not imported, to allow the user to view @@ -597,7 +627,7 @@ def upload_single_file(request, book, book_id): # Queue uploader info uploadText=_(u"File format %(ext)s added to %(book)s", ext=file_ext.upper(), book=book.title) - WorkerThread.add(current_user.nickname, TaskUpload( + WorkerThread.add(current_user.name, TaskUpload( "" + uploadText + "")) return uploader.process( @@ -621,6 +651,46 @@ def upload_cover(request, book): return None +def handle_title_on_edit(book, book_title): + # handle book title + book_title = book_title.rstrip().strip() + if book.title != book_title: + if book_title == '': + book_title = _(u'Unknown') + book.title = book_title + return True + return False + + +def handle_author_on_edit(book, author_name, update_stored=True): + # handle author(s) + input_authors = author_name.split('&') + input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) + # Remove duplicates in authors list + input_authors = helper.uniq(input_authors) + # we have all author names now + if input_authors == ['']: + input_authors = [_(u'Unknown')] # prevent empty Author + + change = modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author') + + # Search for each author if author is in database, if not, author name and sorted author name is generated new + # everything then is assembled for sorted author field in database + sort_authors_list = list() + for inp in input_authors: + stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first() + if not stored_author: + stored_author = helper.get_sorted_author(inp) + else: + stored_author = stored_author.sort + sort_authors_list.append(helper.get_sorted_author(stored_author)) + sort_authors = ' & '.join(sort_authors_list) + if book.author_sort != sort_authors and update_stored: + book.author_sort = sort_authors + change = True + return input_authors, change + + @editbook.route("/admin/book/", methods=['GET', 'POST']) @login_required_if_no_ano @edit_required @@ -638,7 +708,6 @@ def edit_book(book_id): if request.method != 'POST': return render_edit_book(book_id) - book = calibre_db.get_filtered_book(book_id, allow_show_archived=True) # Book not found @@ -656,41 +725,14 @@ def edit_book(book_id): # Update book edited_books_id = None - #handle book title - if book.title != to_save["book_title"].rstrip().strip(): - if to_save["book_title"] == '': - to_save["book_title"] = _(u'Unknown') - book.title = to_save["book_title"].rstrip().strip() + # handle book title + title_change = handle_title_on_edit(book, to_save["book_title"]) + + input_authors, authorchange = handle_author_on_edit(book, to_save["author_name"]) + if authorchange or title_change: edited_books_id = book.id modif_date = True - # handle author(s) - input_authors = to_save["author_name"].split('&') - input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) - # Remove duplicates in authors list - input_authors = helper.uniq(input_authors) - # we have all author names now - if input_authors == ['']: - input_authors = [_(u'Unknown')] # prevent empty Author - - modif_date |= modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author') - - # Search for each author if author is in database, if not, authorname and sorted authorname is generated new - # everything then is assembled for sorted author field in database - sort_authors_list = list() - for inp in input_authors: - stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first() - if not stored_author: - stored_author = helper.get_sorted_author(inp) - else: - stored_author = stored_author.sort - sort_authors_list.append(helper.get_sorted_author(stored_author)) - sort_authors = ' & '.join(sort_authors_list) - if book.author_sort != sort_authors: - edited_books_id = book.id - book.author_sort = sort_authors - modif_date = True - if config.config_use_google_drive: gdriveutils.updateGdriveCalibreFromLocal() @@ -715,10 +757,8 @@ def edit_book(book_id): # Add default series_index to book modif_date |= edit_book_series_index(to_save["series_index"], book) - # Handle book comments/description modif_date |= edit_book_comments(to_save["description"], book) - # Handle identifiers input_identifiers = identifier_list(to_save, book) modification, warning = modify_identifiers(input_identifiers, book.identifiers, calibre_db.session) @@ -727,9 +767,16 @@ def edit_book(book_id): modif_date |= modification # Handle book tags modif_date |= edit_book_tags(to_save['tags'], book) - # Handle book series modif_date |= edit_book_series(to_save["series"], book) + # handle book publisher + modif_date |= edit_book_publisher(to_save['publisher'], book) + # handle book languages + modif_date |= edit_book_languages(to_save['languages'], book) + # handle book ratings + modif_date |= edit_book_ratings(to_save, book) + # handle cc data + modif_date |= edit_cc_data(book_id, book, to_save) if to_save["pubdate"]: try: @@ -739,18 +786,6 @@ def edit_book(book_id): else: book.pubdate = db.Books.DEFAULT_PUBDATE - # handle book publisher - modif_date |= edit_book_publisher(to_save['publisher'], book) - - # handle book languages - modif_date |= edit_book_languages(to_save['languages'], book) - - # handle book ratings - modif_date |= edit_book_ratings(to_save, book) - - # handle cc data - modif_date |= edit_cc_data(book_id, book, to_save) - if modif_date: book.last_modified = datetime.utcnow() calibre_db.session.merge(book) @@ -766,8 +801,8 @@ def edit_book(book_id): calibre_db.session.rollback() flash(error, category="error") return render_edit_book(book_id) - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) calibre_db.session.rollback() flash(_("Error editing book, please check logfile for details"), category="error") return redirect(url_for('web.show_book', book_id=book.id)) @@ -883,6 +918,48 @@ def create_book_on_upload(modif_date, meta): calibre_db.session.flush() return db_book, input_authors, title_dir +def file_handling_on_upload(requested_file): + # check if file extension is correct + if '.' in requested_file.filename: + file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() + if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD: + flash( + _("File extension '%(ext)s' is not allowed to be uploaded to this server", + ext=file_ext), category="error") + return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + else: + flash(_('File to be uploaded must have an extension'), category="error") + return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + + # extract metadata from file + try: + meta = uploader.upload(requested_file, config.config_rarfile_location) + except (IOError, OSError): + log.error("File %s could not saved to temp dir", requested_file.filename) + flash(_(u"File %(filename)s could not saved to temp dir", + filename=requested_file.filename), category="error") + return None, Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + return meta, None + + +def move_coverfile(meta, db_book): + # move cover to final directory, including book id + if meta.cover: + coverfile = meta.cover + else: + coverfile = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg') + new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg") + try: + copyfile(coverfile, new_coverpath) + if meta.cover: + os.unlink(meta.cover) + except OSError as e: + log.error("Failed to move cover file %s: %s", new_coverpath, e) + flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_coverpath, + error=e), + category="error") + + @editbook.route("/upload", methods=["GET", "POST"]) @login_required_if_no_ano @upload_required @@ -897,30 +974,13 @@ def upload(): calibre_db.update_title_sort(config) calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4())) - # check if file extension is correct - if '.' in requested_file.filename: - file_ext = requested_file.filename.rsplit('.', 1)[-1].lower() - if file_ext not in constants.EXTENSIONS_UPLOAD and '' not in constants.EXTENSIONS_UPLOAD: - flash( - _("File extension '%(ext)s' is not allowed to be uploaded to this server", - ext=file_ext), category="error") - return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') - else: - flash(_('File to be uploaded must have an extension'), category="error") - return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') - - # extract metadata from file - try: - meta = uploader.upload(requested_file, config.config_rarfile_location) - except (IOError, OSError): - log.error("File %s could not saved to temp dir", requested_file.filename) - flash(_(u"File %(filename)s could not saved to temp dir", - filename= requested_file.filename), category="error") - return Response(json.dumps({"location": url_for("web.index")}), mimetype='application/json') + meta, error = file_handling_on_upload(requested_file) + if error: + return error db_book, input_authors, title_dir = create_book_on_upload(modif_date, meta) - # Comments needs book id therfore only possible after flush + # Comments needs book id therefore only possible after flush modif_date |= edit_book_comments(Markup(meta.description).unescape(), db_book) book_id = db_book.id @@ -932,21 +992,7 @@ def upload(): meta.file_path, title_dir + meta.extension) - # move cover to final directory, including book id - if meta.cover: - coverfile = meta.cover - else: - coverfile = os.path.join(constants.STATIC_DIR, 'generic_cover.jpg') - new_coverpath = os.path.join(config.config_calibre_dir, db_book.path, "cover.jpg") - try: - copyfile(coverfile, new_coverpath) - if meta.cover: - os.unlink(meta.cover) - except OSError as e: - log.error("Failed to move cover file %s: %s", new_coverpath, e) - flash(_(u"Failed to Move Cover File %(file)s: %(error)s", file=new_coverpath, - error=e), - category="error") + move_coverfile(meta, db_book) # save data to database, reread data calibre_db.session.commit() @@ -956,7 +1002,7 @@ def upload(): if error: flash(error, category="error") uploadText=_(u"File %(file)s uploaded", file=title) - WorkerThread.add(current_user.nickname, TaskUpload( + WorkerThread.add(current_user.name, TaskUpload( "" + uploadText + "")) if len(request.files.getlist("btn-upload")) < 2: @@ -986,7 +1032,7 @@ def convert_bookformat(book_id): log.info('converting: book id: %s from: %s to: %s', book_id, book_format_from, book_format_to) rtn = helper.convert_book_format(book_id, config.config_calibre_dir, book_format_from.upper(), - book_format_to.upper(), current_user.nickname) + book_format_to.upper(), current_user.name) if rtn is None: flash(_(u"Book successfully queued for converting to %(book_format)s", @@ -996,6 +1042,7 @@ def convert_bookformat(book_id): flash(_(u"There was an error converting this book: %(res)s", res=rtn), category="error") return redirect(url_for('editbook.edit_book', book_id=book_id)) + @editbook.route("/ajax/editbooks/", methods=['POST']) @login_required_if_no_ano @edit_required @@ -1005,71 +1052,79 @@ def edit_list_book(param): ret = "" if param =='series_index': edit_book_series_index(vals['value'], book) - ret = Response(json.dumps({'success':True, 'newValue': book.series_index}), mimetype='application/json') + ret = Response(json.dumps({'success': True, 'newValue': book.series_index}), mimetype='application/json') elif param =='tags': edit_book_tags(vals['value'], book) - ret = Response(json.dumps({'success':True, 'newValue': ', '.join([tag.name for tag in book.tags])}), + ret = Response(json.dumps({'success': True, 'newValue': ', '.join([tag.name for tag in book.tags])}), mimetype='application/json') elif param =='series': edit_book_series(vals['value'], book) - ret = Response(json.dumps({'success':True, 'newValue': ', '.join([serie.name for serie in book.series])}), + ret = Response(json.dumps({'success': True, 'newValue': ', '.join([serie.name for serie in book.series])}), mimetype='application/json') elif param =='publishers': - vals['publisher'] = vals['value'] - edit_book_publisher(vals, book) - ret = Response(json.dumps({'success':True, 'newValue': book.publishers}), + edit_book_publisher(vals['value'], book) + ret = Response(json.dumps({'success': True, + 'newValue': ', '.join([publisher.name for publisher in book.publishers])}), mimetype='application/json') elif param =='languages': - edit_book_languages(vals['value'], book) - # ToDo: Not working - ret = Response(json.dumps({'success':True, 'newValue': ', '.join([lang.name for lang in book.languages])}), - mimetype='application/json') + invalid = list() + edit_book_languages(vals['value'], book, invalid=invalid) + if invalid: + ret = Response(json.dumps({'success': False, + 'msg': 'Invalid languages in request: {}'.format(','.join(invalid))}), + mimetype='application/json') + else: + lang_names = list() + for lang in book.languages: + try: + lang_names.append(LC.parse(lang.lang_code).get_language_name(get_locale())) + except UnknownLocaleError: + lang_names.append(_(isoLanguages.get(part3=lang.lang_code).name)) + ret = Response(json.dumps({'success': True, 'newValue': ', '.join(lang_names)}), + mimetype='application/json') elif param =='author_sort': book.author_sort = vals['value'] - ret = Response(json.dumps({'success':True, 'newValue': book.author_sort}), + ret = Response(json.dumps({'success': True, 'newValue': book.author_sort}), mimetype='application/json') - elif param =='title': - book.title = vals['value'] + elif param == 'title': + sort = book.sort + handle_title_on_edit(book, vals.get('value', "")) helper.update_dir_stucture(book.id, config.config_calibre_dir) - ret = Response(json.dumps({'success':True, 'newValue': book.title}), + ret = Response(json.dumps({'success': True, 'newValue': book.title}), mimetype='application/json') elif param =='sort': book.sort = vals['value'] - ret = Response(json.dumps({'success':True, 'newValue': book.sort}), + ret = Response(json.dumps({'success': True, 'newValue': book.sort}), mimetype='application/json') - # ToDo: edit books elif param =='authors': - input_authors = vals['value'].split('&') - input_authors = list(map(lambda it: it.strip().replace(',', '|'), input_authors)) - modify_database_object(input_authors, book.authors, db.Authors, calibre_db.session, 'author') - sort_authors_list = list() - for inp in input_authors: - stored_author = calibre_db.session.query(db.Authors).filter(db.Authors.name == inp).first() - if not stored_author: - stored_author = helper.get_sorted_author(inp) - else: - stored_author = stored_author.sort - sort_authors_list.append(helper.get_sorted_author(stored_author)) - sort_authors = ' & '.join(sort_authors_list) - if book.author_sort != sort_authors: - book.author_sort = sort_authors + input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true") helper.update_dir_stucture(book.id, config.config_calibre_dir, input_authors[0]) - ret = Response(json.dumps({'success':True, 'newValue': ', '.join([author.name for author in book.authors])}), + ret = Response(json.dumps({'success': True, + 'newValue': ' & '.join([author.replace('|',',') for author in input_authors])}), mimetype='application/json') book.last_modified = datetime.utcnow() calibre_db.session.commit() + # revert change for sort if automatic fields link is deactivated + if param == 'title' and vals.get('checkT') == "false": + book.sort = sort + calibre_db.session.commit() return ret + @editbook.route("/ajax/sort_value//") @login_required def get_sorted_entry(field, bookid): - if field == 'title' or field == 'authors': + if field in ['title', 'authors', 'sort', 'author_sort']: book = calibre_db.get_filtered_book(bookid) if book: if field == 'title': return json.dumps({'sort': book.sort}) elif field == 'authors': return json.dumps({'author_sort': book.author_sort}) + if field == 'sort': + return json.dumps({'sort': book.title}) + if field == 'author_sort': + return json.dumps({'author_sort': book.author}) return "" diff --git a/cps/epub.py b/cps/epub.py index 5833c2aa..998dbfa6 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -87,18 +87,29 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): lang = epub_metadata['language'].split('-', 1)[0].lower() epub_metadata['language'] = isoLanguages.get_lang3(lang) - series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns) - if len(series) > 0: - epub_metadata['series'] = series[0] - else: - epub_metadata['series'] = '' + epub_metadata = parse_epbub_series(ns, tree, epub_metadata) - series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns) - if len(series_id) > 0: - epub_metadata['series_id'] = series_id[0] - else: - epub_metadata['series_id'] = '1' + coverfile = parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path) + if not epub_metadata['title']: + title = original_file_name + else: + title = epub_metadata['title'] + + return BookMeta( + file_path=tmp_file_path, + extension=original_file_extension, + title=title.encode('utf-8').decode('utf-8'), + author=epub_metadata['creator'].encode('utf-8').decode('utf-8'), + cover=coverfile, + description=epub_metadata['description'], + tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'), + series=epub_metadata['series'].encode('utf-8').decode('utf-8'), + series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'), + languages=epub_metadata['language'], + publisher="") + +def parse_ebpub_cover(ns, tree, epubZip, coverpath, tmp_file_path): coversection = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns) coverfile = None if len(coversection) > 0: @@ -126,21 +137,18 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension): coverfile = extractCover(epubZip, filename, "", tmp_file_path) else: coverfile = extractCover(epubZip, coversection[0], coverpath, tmp_file_path) + return coverfile - if not epub_metadata['title']: - title = original_file_name +def parse_epbub_series(ns, tree, epub_metadata): + series = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series']/@content", namespaces=ns) + if len(series) > 0: + epub_metadata['series'] = series[0] else: - title = epub_metadata['title'] + epub_metadata['series'] = '' - return BookMeta( - file_path=tmp_file_path, - extension=original_file_extension, - title=title.encode('utf-8').decode('utf-8'), - author=epub_metadata['creator'].encode('utf-8').decode('utf-8'), - cover=coverfile, - description=epub_metadata['description'], - tags=epub_metadata['subject'].encode('utf-8').decode('utf-8'), - series=epub_metadata['series'].encode('utf-8').decode('utf-8'), - series_id=epub_metadata['series_id'].encode('utf-8').decode('utf-8'), - languages=epub_metadata['language'], - publisher="") + series_id = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='calibre:series_index']/@content", namespaces=ns) + if len(series_id) > 0: + epub_metadata['series_id'] = series_id[0] + else: + epub_metadata['series_id'] = '1' + return epub_metadata diff --git a/cps/gdrive.py b/cps/gdrive.py index 158a5a4f..6d6dfd07 100644 --- a/cps/gdrive.py +++ b/cps/gdrive.py @@ -155,6 +155,6 @@ def on_received_watch_confirmation(): # prevent error on windows, as os.rename does on existing files, also allow cross hdd move move(os.path.join(tmp_dir, "tmp_metadata.db"), dbpath) calibre_db.reconnect_db(config, ub.app_DB_path) - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) return '' diff --git a/cps/gdriveutils.py b/cps/gdriveutils.py index 79587b79..a98d0b66 100644 --- a/cps/gdriveutils.py +++ b/cps/gdriveutils.py @@ -28,7 +28,11 @@ from sqlalchemy import create_engine from sqlalchemy import Column, UniqueConstraint from sqlalchemy import String, Integer from sqlalchemy.orm import sessionmaker, scoped_session -from sqlalchemy.ext.declarative import declarative_base +try: + # Compability with sqlalchemy 2.0 + from sqlalchemy.orm import declarative_base +except ImportError: + from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.exc import OperationalError, InvalidRequestError try: @@ -198,8 +202,8 @@ def getDrive(drive=None, gauth=None): gauth.Refresh() except RefreshError as e: log.error("Google Drive error: %s", e) - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) else: # Initialize the saved creds gauth.Authorize() @@ -493,8 +497,8 @@ def getChangeById (drive, change_id): except (errors.HttpError) as error: log.error(error) return None - except Exception as e: - log.error(e) + except Exception as ex: + log.error(ex) return None diff --git a/cps/helper.py b/cps/helper.py index e18ae33b..f1c32ea0 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -35,7 +35,7 @@ from babel.units import format_unit from flask import send_from_directory, make_response, redirect, abort, url_for from flask_babel import gettext as _ from flask_login import current_user -from sqlalchemy.sql.expression import true, false, and_, text +from sqlalchemy.sql.expression import true, false, and_, text, func from werkzeug.datastructures import Headers from werkzeug.security import generate_password_hash @@ -480,8 +480,8 @@ def reset_password(user_id): password = generate_random_password() existing_user.password = generate_password_hash(password) ub.session.commit() - send_registration_mail(existing_user.email, existing_user.nickname, password, True) - return 1, existing_user.nickname + send_registration_mail(existing_user.email, existing_user.name, password, True) + return 1, existing_user.name except Exception: ub.session.rollback() return 0, None @@ -498,11 +498,37 @@ def generate_random_password(): def uniq(inpt): output = [] + inpt = [ " ".join(inp.split()) for inp in inpt] for x in inpt: if x not in output: output.append(x) return output +def check_email(email): + email = valid_email(email) + if ub.session.query(ub.User).filter(func.lower(ub.User.email) == email.lower()).first(): + log.error(u"Found an existing account for this e-mail address") + raise Exception(_(u"Found an existing account for this e-mail address")) + return email + + +def check_username(username): + username = username.strip() + if ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.lower()).scalar(): + log.error(u"This username is already taken") + raise Exception (_(u"This username is already taken")) + return username + + +def valid_email(email): + email = email.strip() + # Regex according to https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email#validation + if not re.search(r"^[\w.!#$%&'*+\\/=?^_`{|}~-]+@[\w](?:[\w-]{0,61}[\w])?(?:\.[\w](?:[\w-]{0,61}[\w])?)*$", + email): + log.error(u"Invalid e-mail address format") + raise Exception(_(u"Invalid e-mail address format")) + return email + # ################################# External interface ################################# @@ -550,8 +576,8 @@ def get_book_cover_internal(book, use_generic_cover_on_failure): else: log.error('%s/cover.jpg not found on Google Drive', book.path) return get_cover_on_failure(use_generic_cover_on_failure) - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) return get_cover_on_failure(use_generic_cover_on_failure) else: cover_file_path = os.path.join(config.config_calibre_dir, book.path) @@ -731,7 +757,7 @@ def format_runtime(runtime): def render_task_status(tasklist): renderedtasklist = list() for __, user, __, task in tasklist: - if user == current_user.nickname or current_user.role_admin(): + if user == current_user.name or current_user.role_admin(): ret = {} if task.start_time: ret['starttime'] = format_datetime(task.start_time, format='short', locale=get_locale()) diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py index 08bdf956..35d9f0a7 100644 --- a/cps/isoLanguages.py +++ b/cps/isoLanguages.py @@ -63,11 +63,12 @@ def get_language_codes(locale, language_names, remainder=None): if v in language_names: lang.append(k) language_names.remove(v) - if remainder is not None: + if remainder is not None and language_names: remainder.extend(language_names) return lang + def get_valid_language_codes(locale, language_names, remainder=None): lang = list() if "" in language_names: diff --git a/cps/jinjia.py b/cps/jinjia.py index 688d1fba..f37dfb49 100644 --- a/cps/jinjia.py +++ b/cps/jinjia.py @@ -82,7 +82,7 @@ def formatdate_filter(val): except AttributeError as e: log.error('Babel error: %s, Current user locale: %s, Current User: %s', e, current_user.locale, - current_user.nickname + current_user.name ) return val diff --git a/cps/kobo.py b/cps/kobo.py index 2b956bfc..8988ef3f 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -177,7 +177,7 @@ def HandleSyncRequest(): for book in changed_entries: formats = [data.format for data in book.Books.data] if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: - helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.nickname) + helper.convert_book_format(book.Books.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) kobo_reading_state = get_or_create_reading_state(book.Books.id) entitlement = { @@ -262,8 +262,8 @@ def generate_sync_response(sync_token, sync_results, set_cont=False): extra_headers["x-kobo-sync-mode"] = store_response.headers.get("x-kobo-sync-mode") extra_headers["x-kobo-recent-reads"] = store_response.headers.get("x-kobo-recent-reads") - except Exception as e: - log.error("Failed to receive or parse response from Kobo's sync endpoint: " + str(e)) + except Exception as ex: + log.error("Failed to receive or parse response from Kobo's sync endpoint: {}".format(ex)) if set_cont: extra_headers["x-kobo-sync"] = "continue" sync_token.to_headers(extra_headers) diff --git a/cps/kobo_auth.py b/cps/kobo_auth.py index 23f60fe2..a51095c8 100644 --- a/cps/kobo_auth.py +++ b/cps/kobo_auth.py @@ -155,7 +155,7 @@ def generate_auth_token(user_id): for book in books: formats = [data.format for data in book.data] if not 'KEPUB' in formats and config.config_kepubifypath and 'EPUB' in formats: - helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.nickname) + helper.convert_book_format(book.id, config.config_calibre_dir, 'EPUB', 'KEPUB', current_user.name) return render_title_template( "generate_kobo_auth_url.html", diff --git a/cps/oauth_bb.py b/cps/oauth_bb.py index 1fd7c9b1..5d909d91 100644 --- a/cps/oauth_bb.py +++ b/cps/oauth_bb.py @@ -42,6 +42,7 @@ except NameError: oauth_check = {} +oauthblueprints = [] oauth = Blueprint('oauth', __name__) log = logger.create() @@ -87,7 +88,7 @@ def register_user_with_oauth(user=None): except NoResultFound: # no found, return error return - ub.session_commit("User {} with OAuth for provider {} registered".format(user.nickname, oauth_key)) + ub.session_commit("User {} with OAuth for provider {} registered".format(user.name, oauth_key)) def logout_oauth_user(): @@ -133,8 +134,8 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider # already bind with user, just login if oauth_entry.user: login_user(oauth_entry.user) - log.debug(u"You are now logged in as: '%s'", oauth_entry.user.nickname) - flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.nickname), + log.debug(u"You are now logged in as: '%s'", oauth_entry.user.name) + flash(_(u"you are now logged in as: '%(nickname)s'", nickname= oauth_entry.user.name), category="success") return redirect(url_for('web.index')) else: @@ -146,8 +147,8 @@ def bind_oauth_or_register(provider_id, provider_user_id, redirect_url, provider ub.session.commit() flash(_(u"Link to %(oauth)s Succeeded", oauth=provider_name), category="success") return redirect(url_for('web.profile')) - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) ub.session.rollback() else: flash(_(u"Login failed, No User Linked With OAuth Account"), category="error") @@ -193,8 +194,8 @@ def unlink_oauth(provider): ub.session.commit() logout_oauth_user() flash(_(u"Unlink to %(oauth)s Succeeded", oauth=oauth_check[provider]), category="success") - except Exception as e: - log.debug_or_exception(e) + except Exception as ex: + log.debug_or_exception(ex) ub.session.rollback() flash(_(u"Unlink to %(oauth)s Failed", oauth=oauth_check[provider]), category="error") except NoResultFound: @@ -203,7 +204,6 @@ def unlink_oauth(provider): return redirect(url_for('web.profile')) def generate_oauth_blueprints(): - oauthblueprints = [] if not ub.session.query(ub.OAuthProvider).count(): for provider in ("github", "google"): oauthProvider = ub.OAuthProvider() @@ -299,39 +299,6 @@ if ub.oauth_support: ) # ToDo: Translate flash(msg, category="error") - - @oauth.route('/link/github') - @oauth_required - def github_login(): - if not github.authorized: - return redirect(url_for('github.login')) - account_info = github.get('/user') - if account_info.ok: - account_info_json = account_info.json() - return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github') - flash(_(u"GitHub Oauth error, please retry later."), category="error") - return redirect(url_for('web.login')) - - - @oauth.route('/unlink/github', methods=["GET"]) - @login_required - def github_login_unlink(): - return unlink_oauth(oauthblueprints[0]['id']) - - - @oauth.route('/link/google') - @oauth_required - def google_login(): - if not google.authorized: - return redirect(url_for("google.login")) - resp = google.get("/oauth2/v2/userinfo") - if resp.ok: - account_info_json = resp.json() - return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google') - flash(_(u"Google Oauth error, please retry later."), category="error") - return redirect(url_for('web.login')) - - @oauth_error.connect_via(oauthblueprints[1]['blueprint']) def google_error(blueprint, error, error_description=None, error_uri=None): msg = ( @@ -346,7 +313,39 @@ if ub.oauth_support: flash(msg, category="error") - @oauth.route('/unlink/google', methods=["GET"]) - @login_required - def google_login_unlink(): - return unlink_oauth(oauthblueprints[1]['id']) +@oauth.route('/link/github') +@oauth_required +def github_login(): + if not github.authorized: + return redirect(url_for('github.login')) + account_info = github.get('/user') + if account_info.ok: + account_info_json = account_info.json() + return bind_oauth_or_register(oauthblueprints[0]['id'], account_info_json['id'], 'github.login', 'github') + flash(_(u"GitHub Oauth error, please retry later."), category="error") + return redirect(url_for('web.login')) + + +@oauth.route('/unlink/github', methods=["GET"]) +@login_required +def github_login_unlink(): + return unlink_oauth(oauthblueprints[0]['id']) + + +@oauth.route('/link/google') +@oauth_required +def google_login(): + if not google.authorized: + return redirect(url_for("google.login")) + resp = google.get("/oauth2/v2/userinfo") + if resp.ok: + account_info_json = resp.json() + return bind_oauth_or_register(oauthblueprints[1]['id'], account_info_json['id'], 'google.login', 'google') + flash(_(u"Google Oauth error, please retry later."), category="error") + return redirect(url_for('web.login')) + + +@oauth.route('/unlink/google', methods=["GET"]) +@login_required +def google_login_unlink(): + return unlink_oauth(oauthblueprints[1]['id']) diff --git a/cps/opds.py b/cps/opds.py index e8d3fad9..85a978a7 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -27,7 +27,7 @@ from functools import wraps from flask import Blueprint, request, render_template, Response, g, make_response, abort from flask_login import current_user -from sqlalchemy.sql.expression import func, text, or_, and_ +from sqlalchemy.sql.expression import func, text, or_, and_, true from werkzeug.security import check_password_hash from . import constants, logger, config, db, calibre_db, ub, services, get_locale, isoLanguages @@ -97,6 +97,44 @@ def feed_normal_search(): return feed_search(request.args.get("query", "").strip()) +@opds.route("/opds/books") +@requires_basic_auth_if_no_ano +def feed_booksindex(): + shift = 0 + off = int(request.args.get("offset") or 0) + entries = calibre_db.session.query(func.upper(func.substr(db.Books.sort, 1, 1)).label('id'))\ + .filter(calibre_db.common_filters()).group_by(func.upper(func.substr(db.Books.sort, 1, 1))).all() + + elements = [] + if off == 0: + elements.append({'id': "00", 'name':_("All")}) + shift = 1 + for entry in entries[ + off + shift - 1: + int(off + int(config.config_books_per_page) - shift)]: + elements.append({'id': entry.id, 'name': entry.id}) + + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(entries) + 1) + return render_xml_template('feed.xml', + letterelements=elements, + folder='opds.feed_letter_books', + pagination=pagination) + + +@opds.route("/opds/books/letter/") +@requires_basic_auth_if_no_ano +def feed_letter_books(book_id): + off = request.args.get("offset") or 0 + letter = true() if book_id == "00" else func.upper(db.Books.sort).startswith(book_id) + entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0, + db.Books, + letter, + [db.Books.sort]) + + return render_xml_template('feed.xml', entries=entries, pagination=pagination) + + @opds.route("/opds/new") @requires_basic_auth_if_no_ano def feed_new(): @@ -150,14 +188,41 @@ def feed_hot(): @opds.route("/opds/author") @requires_basic_auth_if_no_ano def feed_authorindex(): - off = request.args.get("offset") or 0 - entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\ - .filter(calibre_db.common_filters())\ - .group_by(text('books_authors_link.author'))\ - .order_by(db.Authors.sort).limit(config.config_books_per_page)\ - .offset(off) + shift = 0 + off = int(request.args.get("offset") or 0) + entries = calibre_db.session.query(func.upper(func.substr(db.Authors.sort, 1, 1)).label('id'))\ + .join(db.books_authors_link).join(db.Books).filter(calibre_db.common_filters())\ + .group_by(func.upper(func.substr(db.Authors.sort, 1, 1))).all() + + elements = [] + if off == 0: + elements.append({'id': "00", 'name':_("All")}) + shift = 1 + for entry in entries[ + off + shift - 1: + int(off + int(config.config_books_per_page) - shift)]: + elements.append({'id': entry.id, 'name': entry.id}) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(calibre_db.session.query(db.Authors).all())) + len(entries) + 1) + return render_xml_template('feed.xml', + letterelements=elements, + folder='opds.feed_letter_author', + pagination=pagination) + + +@opds.route("/opds/author/letter/") +@requires_basic_auth_if_no_ano +def feed_letter_author(book_id): + off = request.args.get("offset") or 0 + letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id) + entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\ + .filter(calibre_db.common_filters()).filter(letter)\ + .group_by(text('books_authors_link.author'))\ + .order_by(db.Authors.sort) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + entries.count()) + entries = entries.limit(config.config_books_per_page).offset(off).all() return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination) @@ -201,17 +266,41 @@ def feed_publisher(book_id): @opds.route("/opds/category") @requires_basic_auth_if_no_ano def feed_categoryindex(): + shift = 0 + off = int(request.args.get("offset") or 0) + entries = calibre_db.session.query(func.upper(func.substr(db.Tags.name, 1, 1)).label('id'))\ + .join(db.books_tags_link).join(db.Books).filter(calibre_db.common_filters())\ + .group_by(func.upper(func.substr(db.Tags.name, 1, 1))).all() + elements = [] + if off == 0: + elements.append({'id': "00", 'name':_("All")}) + shift = 1 + for entry in entries[ + off + shift - 1: + int(off + int(config.config_books_per_page) - shift)]: + elements.append({'id': entry.id, 'name': entry.id}) + + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(entries) + 1) + return render_xml_template('feed.xml', + letterelements=elements, + folder='opds.feed_letter_category', + pagination=pagination) + +@opds.route("/opds/category/letter/") +@requires_basic_auth_if_no_ano +def feed_letter_category(book_id): off = request.args.get("offset") or 0 + letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id) entries = calibre_db.session.query(db.Tags)\ .join(db.books_tags_link)\ .join(db.Books)\ - .filter(calibre_db.common_filters())\ + .filter(calibre_db.common_filters()).filter(letter)\ .group_by(text('books_tags_link.tag'))\ - .order_by(db.Tags.name)\ - .offset(off)\ - .limit(config.config_books_per_page) + .order_by(db.Tags.name) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(calibre_db.session.query(db.Tags).all())) + entries.count()) + entries = entries.offset(off).limit(config.config_books_per_page).all() return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination) @@ -229,16 +318,40 @@ def feed_category(book_id): @opds.route("/opds/series") @requires_basic_auth_if_no_ano def feed_seriesindex(): + shift = 0 + off = int(request.args.get("offset") or 0) + entries = calibre_db.session.query(func.upper(func.substr(db.Series.sort, 1, 1)).label('id'))\ + .join(db.books_series_link).join(db.Books).filter(calibre_db.common_filters())\ + .group_by(func.upper(func.substr(db.Series.sort, 1, 1))).all() + elements = [] + if off == 0: + elements.append({'id': "00", 'name':_("All")}) + shift = 1 + for entry in entries[ + off + shift - 1: + int(off + int(config.config_books_per_page) - shift)]: + elements.append({'id': entry.id, 'name': entry.id}) + pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, + len(entries) + 1) + return render_xml_template('feed.xml', + letterelements=elements, + folder='opds.feed_letter_series', + pagination=pagination) + +@opds.route("/opds/series/letter/") +@requires_basic_auth_if_no_ano +def feed_letter_series(book_id): off = request.args.get("offset") or 0 + letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id) entries = calibre_db.session.query(db.Series)\ .join(db.books_series_link)\ .join(db.Books)\ - .filter(calibre_db.common_filters())\ + .filter(calibre_db.common_filters()).filter(letter)\ .group_by(text('books_series_link.series'))\ - .order_by(db.Series.sort)\ - .offset(off).all() + .order_by(db.Series.sort) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, - len(calibre_db.session.query(db.Series).all())) + entries.count()) + entries = entries.offset(off).limit(config.config_books_per_page).all() return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination) @@ -269,7 +382,7 @@ def feed_ratingindex(): len(entries)) element = list() for entry in entries: - element.append(FeedObject(entry[0].id, "{} Stars".format(entry.name))) + element.append(FeedObject(entry[0].id, _("{} Stars").format(entry.name))) return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination) @@ -428,7 +541,7 @@ def check_auth(username, password): username = username.encode('windows-1252') except UnicodeEncodeError: username = username.encode('utf-8') - user = ub.session.query(ub.User).filter(func.lower(ub.User.nickname) == + user = ub.session.query(ub.User).filter(func.lower(ub.User.name) == username.decode('utf-8').lower()).first() if bool(user and check_password_hash(str(user.password), password)): return True diff --git a/cps/remotelogin.py b/cps/remotelogin.py index d9e7388f..47d10c20 100644 --- a/cps/remotelogin.py +++ b/cps/remotelogin.py @@ -126,11 +126,11 @@ def token_verified(): login_user(user) ub.session.delete(auth_token) - ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.nickname)) + ub.session_commit("User {} logged in via remotelogin, token deleted".format(user.name)) data['status'] = 'success' log.debug(u"Remote Login for userid %s succeded", user.id) - flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.nickname), category="success") + flash(_(u"you are now logged in as: '%(nickname)s'", nickname=user.name), category="success") response = make_response(json.dumps(data, ensure_ascii=False)) response.headers["Content-Type"] = "application/json; charset=utf-8" diff --git a/cps/render_template.py b/cps/render_template.py index fdd8abb7..51e4db95 100644 --- a/cps/render_template.py +++ b/cps/render_template.py @@ -42,10 +42,16 @@ def get_sidebar_config(kwargs=None): sidebar.append({"glyph": "glyphicon-fire", "text": _('Hot Books'), "link": 'web.books_list', "id": "hot", "visibility": constants.SIDEBAR_HOT, 'public': True, "page": "hot", "show_text": _('Show Hot Books'), "config_show": True}) - sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list', - "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous), - "page": "download", "show_text": _('Show Downloaded Books'), - "config_show": content}) + if current_user.role_admin(): + sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.download_list', + "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous), + "page": "download", "show_text": _('Show Downloaded Books'), + "config_show": content}) + else: + sidebar.append({"glyph": "glyphicon-download", "text": _('Downloaded Books'), "link": 'web.books_list', + "id": "download", "visibility": constants.SIDEBAR_DOWNLOAD, 'public': (not g.user.is_anonymous), + "page": "download", "show_text": _('Show Downloaded Books'), + "config_show": content}) sidebar.append( {"glyph": "glyphicon-star", "text": _('Top Rated Books'), "link": 'web.books_list', "id": "rated", "visibility": constants.SIDEBAR_BEST_RATED, 'public': True, "page": "rated", diff --git a/cps/services/__init__.py b/cps/services/__init__.py index 17f1f529..efd55621 100644 --- a/cps/services/__init__.py +++ b/cps/services/__init__.py @@ -45,3 +45,9 @@ except ImportError as err: log.debug("Cannot import SyncToken, syncing books with Kobo Devices will not work: %s", err) kobo = None SyncToken = None + +try: + from . import gmail +except ImportError as err: + log.debug("Cannot import Gmail, sending books via G-Mail Accounts will not work: %s", err) + gmail = None diff --git a/cps/services/gmail.py b/cps/services/gmail.py new file mode 100644 index 00000000..9524dd75 --- /dev/null +++ b/cps/services/gmail.py @@ -0,0 +1,80 @@ +from __future__ import print_function +import os.path +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request +from googleapiclient.discovery import build +from google.oauth2.credentials import Credentials + +from datetime import datetime +import base64 +from flask_babel import gettext as _ +from ..constants import BASE_DIR +from .. import logger + + +log = logger.create() + +SCOPES = ['openid', 'https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/userinfo.email'] + +def setup_gmail(token): + # If there are no (valid) credentials available, let the user log in. + creds = None + if "token" in token: + creds = Credentials( + token=token['token'], + refresh_token=token['refresh_token'], + token_uri=token['token_uri'], + client_id=token['client_id'], + client_secret=token['client_secret'], + scopes=token['scopes'], + ) + creds.expiry = datetime.fromisoformat(token['expiry']) + + if not creds or not creds.valid: + # don't forget to dump one more time after the refresh + # also, some file-locking routines wouldn't be needless + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + cred_file = os.path.join(BASE_DIR, 'gmail.json') + if not os.path.exists(cred_file): + raise Exception(_("Found no valid gmail.json file with OAuth information")) + flow = InstalledAppFlow.from_client_secrets_file( + os.path.join(BASE_DIR, 'gmail.json'), SCOPES) + creds = flow.run_local_server(port=0) + user_info = get_user_info(creds) + return { + 'token': creds.token, + 'refresh_token': creds.refresh_token, + 'token_uri': creds.token_uri, + 'client_id': creds.client_id, + 'client_secret': creds.client_secret, + 'scopes': creds.scopes, + 'expiry': creds.expiry.isoformat(), + 'email': user_info + } + +def get_user_info(credentials): + user_info_service = build(serviceName='oauth2', version='v2',credentials=credentials) + user_info = user_info_service.userinfo().get().execute() + return user_info.get('email', "") + +def send_messsage(token, msg): + creds = Credentials( + token=token['token'], + refresh_token=token['refresh_token'], + token_uri=token['token_uri'], + client_id=token['client_id'], + client_secret=token['client_secret'], + scopes=token['scopes'], + ) + creds.expiry = datetime.fromisoformat(token['expiry']) + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + service = build('gmail', 'v1', credentials=creds) + message_as_bytes = msg.as_bytes() # the message should converted from string to bytes. + message_as_base64 = base64.urlsafe_b64encode(message_as_bytes) # encode in base64 (printable letters coding) + raw = message_as_base64.decode() # convert to something JSON serializable + body = {'raw': raw} + + (service.users().messages().send(userId='me', body=body).execute()) diff --git a/cps/services/worker.py b/cps/services/worker.py index 072674a0..8433e408 100644 --- a/cps/services/worker.py +++ b/cps/services/worker.py @@ -159,9 +159,9 @@ class CalibreTask: # catch any unhandled exceptions in a task and automatically fail it try: self.run(*args) - except Exception as e: - self._handleError(str(e)) - log.debug_or_exception(e) + except Exception as ex: + self._handleError(str(ex)) + log.debug_or_exception(ex) self.end_time = datetime.now() diff --git a/cps/shelf.py b/cps/shelf.py index 5c6037ac..68e8331b 100644 --- a/cps/shelf.py +++ b/cps/shelf.py @@ -255,13 +255,13 @@ def create_edit_shelf(shelf, title, page, shelf_id=False): log.info(u"Shelf {} {}".format(to_save["title"], shelf_action)) flash(flash_text, category="success") return redirect(url_for('shelf.show_shelf', shelf_id=shelf.id)) - except (OperationalError, InvalidRequestError) as e: + except (OperationalError, InvalidRequestError) as ex: ub.session.rollback() - log.debug_or_exception(e) + log.debug_or_exception(ex) flash(_(u"Settings DB is not Writeable"), category="error") - except Exception as e: + except Exception as ex: ub.session.rollback() - log.debug_or_exception(e) + log.debug_or_exception(ex) flash(_(u"There was an error"), category="error") return render_title_template('shelf_edit.html', shelf=shelf, title=title, page=page) diff --git a/cps/static/css/img/clear.png b/cps/static/css/img/clear.png new file mode 100644 index 00000000..580b52a5 Binary files /dev/null and b/cps/static/css/img/clear.png differ diff --git a/cps/static/css/img/loading.gif b/cps/static/css/img/loading.gif new file mode 100644 index 00000000..5b33f7e5 Binary files /dev/null and b/cps/static/css/img/loading.gif differ diff --git a/cps/static/css/style.css b/cps/static/css/style.css index 02658771..f1d38901 100644 --- a/cps/static/css/style.css +++ b/cps/static/css/style.css @@ -56,6 +56,7 @@ a, .book-remove, .editable-empty, .editable-empty:hover { color: #45b29d; } +.book-remove:hover { color: #23527c; } .user-remove:hover { color: #23527c; } .btn-default a { color: #444; } .panel-title > a { text-decoration: none; } diff --git a/cps/static/js/filter_grid.js b/cps/static/js/filter_grid.js index d57d155f..623ffdc1 100644 --- a/cps/static/js/filter_grid.js +++ b/cps/static/js/filter_grid.js @@ -15,6 +15,8 @@ * along with this program. If not, see . */ +var direction = $("#asc").data('order'); // 0=Descending order; 1= ascending order + var $list = $("#list").isotope({ itemSelector: ".book", layoutMode: "fitRows", @@ -24,6 +26,9 @@ var $list = $("#list").isotope({ }); $("#desc").click(function() { + if (direction === 0) { + return; + } var page = $(this).data("id"); $.ajax({ method:"post", @@ -39,6 +44,9 @@ $("#desc").click(function() { }); $("#asc").click(function() { + if (direction === 1) { + return; + } var page = $(this).data("id"); $.ajax({ method:"post", diff --git a/cps/static/js/filter_list.js b/cps/static/js/filter_list.js index ef7639fa..b8f79f4e 100644 --- a/cps/static/js/filter_list.js +++ b/cps/static/js/filter_list.js @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -var direction = 0; // Descending order +var direction = $("#asc").data('order'); // 0=Descending order; 1= ascending order var sort = 0; // Show sorted entries $("#sort_name").click(function() { diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 59a658d2..11ce6ed1 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -114,26 +114,23 @@ $(document).ready(function() { } }); -function confirmDialog(id, dataValue, yesFn, noFn) { - var $confirm = $("#GeneralDeleteModal"); - // var dataValue= e.data('value'); // target.data('value'); +function confirmDialog(id, dialogid, dataValue, yesFn, noFn) { + var $confirm = $("#" + dialogid); $confirm.modal('show'); $.ajax({ method:"get", dataType: "json", url: getPath() + "/ajax/loaddialogtexts/" + id, success: function success(data) { - $("#header").html(data.header); - $("#text").html(data.main); + $("#header-"+ dialogid).html(data.header); + $("#text-"+ dialogid).html(data.main); } }); - - - $("#btnConfirmYes").off('click').click(function () { + $("#btnConfirmYes-"+ dialogid).off('click').click(function () { yesFn(dataValue); $confirm.modal("hide"); }); - $("#btnConfirmNo").off('click').click(function () { + $("#btnConfirmNo-"+ dialogid).off('click').click(function () { if (typeof noFn !== 'undefined') { noFn(dataValue); } @@ -485,6 +482,7 @@ $(function() { $("#config_delete_kobo_token").click(function() { confirmDialog( $(this).attr('id'), + "GeneralDeleteModal", $(this).data('value'), function (value) { $.ajax({ @@ -513,6 +511,7 @@ $(function() { $("#btndeluser").click(function() { confirmDialog( $(this).attr('id'), + "GeneralDeleteModal", $(this).data('value'), function(value){ var subform = $('#user_submit').closest("form"); @@ -531,6 +530,7 @@ $(function() { $("#delete_shelf").click(function() { confirmDialog( $(this).attr('id'), + "GeneralDeleteModal", $(this).data('value'), function(value){ window.location.href = window.location.pathname + "/../../shelf/delete/" + value diff --git a/cps/static/js/table.js b/cps/static/js/table.js index 758e8fb3..96901d0b 100644 --- a/cps/static/js/table.js +++ b/cps/static/js/table.js @@ -95,17 +95,22 @@ $(function() { mode: "inline", emptytext: "", success: function (response, __) { - if(!response.success) return response.msg; + if (!response.success) return response.msg; return {newValue: response.newValue}; + }, + params: function (params) { + params.checkA = $('#autoupdate_authorsort').prop('checked'); + params.checkT = $('#autoupdate_titlesort').prop('checked'); + return params } } }; - } - var validateText = $(this).attr("data-edit-validate"); - if (validateText) { - element.editable.validate = function (value) { - if ($.trim(value) === "") return validateText; - }; + var validateText = $(this).attr("data-edit-validate"); + if (validateText) { + element.editable.validate = function (value) { + if ($.trim(value) === "") return validateText; + }; + } } column.push(element); }); @@ -121,7 +126,7 @@ $(function() { search: true, showColumns: true, searchAlign: "left", - showSearchButton : false, + showSearchButton : true, searchOnEnterKey: true, checkboxHeader: false, maintainMetaData: true, @@ -132,7 +137,8 @@ $(function() { }, // eslint-disable-next-line no-unused-vars onEditableSave: function (field, row, oldvalue, $el) { - if (field === "title" || field === "authors") { + if ($.inArray(field, [ "title", "sort" ]) !== -1 && $('#autoupdate_titlesort').prop('checked') + || $.inArray(field, [ "authors", "author_sort" ]) !== -1 && $('#autoupdate_authorsort').prop('checked')) { $.ajax({ method:"get", dataType: "json", @@ -240,16 +246,16 @@ $(function() { } $("#domain-allow-table").on("click-cell.bs.table", function (field, value, row, $element) { if (value === 2) { - confirmDialog("btndeletedomain", $element.id, domainHandle); + confirmDialog("btndeletedomain", "GeneralDeleteModal", $element.id, domainHandle); } }); $("#domain-deny-table").on("click-cell.bs.table", function (field, value, row, $element) { if (value === 2) { - confirmDialog("btndeletedomain", $element.id, domainHandle); + confirmDialog("btndeletedomain", "GeneralDeleteModal", $element.id, domainHandle); } }); - $("#restrictModal").on("hidden.bs.modal", function () { + $("#restrictModal").on("hidden.bs.modal", function (e) { // Destroy table and remove hooks for buttons $("#restrict-elements-table").unbind(); $("#restrict-elements-table").bootstrapTable("destroy"); @@ -258,8 +264,54 @@ $(function() { $("#h2").addClass("hidden"); $("#h3").addClass("hidden"); $("#h4").addClass("hidden"); + $("#add_element").val(""); }); - function startTable(type, userId) { + + function startTable(target, userId) { + var type = 0; + switch(target) { + case "get_column_values": + type = 1; + $("#h2").removeClass("hidden"); + break; + case "get_tags": + type = 0; + $("#h1").removeClass("hidden"); + break; + case "get_user_column_values": + type = 3; + $("#h4").removeClass("hidden"); + break; + case "get_user_tags": + type = 2; + $("#h3").removeClass("hidden"); + break; + case "denied_tags": + type = 2; + $("#h2").removeClass("hidden"); + $("#submit_allow").addClass("hidden"); + $("#submit_restrict").removeClass("hidden"); + break; + case "allowed_tags": + type = 2; + $("#h2").removeClass("hidden"); + $("#submit_restrict").addClass("hidden"); + $("#submit_allow").removeClass("hidden"); + break; + case "allowed_column_value": + type = 3; + $("#h2").removeClass("hidden"); + $("#submit_restrict").addClass("hidden"); + $("#submit_allow").removeClass("hidden"); + break; + case "denied_column_value": + type = 3; + $("#h2").removeClass("hidden"); + $("#submit_allow").addClass("hidden"); + $("#submit_restrict").removeClass("hidden"); + break; + } + $("#restrict-elements-table").bootstrapTable({ formatNoMatches: function () { return ""; @@ -273,6 +325,10 @@ $(function() { return {classes: "bg-dark-danger"}; } }, + onLoadSuccess: function () { + $(".no-records-found").addClass("hidden"); + $(".fixed-table-loading").addClass("hidden"); + }, onClickCell: function (field, value, row) { if (field === 3) { $.ajax ({ @@ -326,28 +382,192 @@ $(function() { return; }); } - $("#get_column_values").on("click", function() { - startTable(1, 0); - $("#h2").removeClass("hidden"); + + $("#restrictModal").on("show.bs.modal", function(e) { + var target = $(e.relatedTarget).attr('id'); + var dataId; + $(e.relatedTarget).one('focus', function(e){$(this).blur();}); + //$(e.relatedTarget).blur(); + if ($(e.relatedTarget).hasClass("button_head")) { + dataId = $('#user-table').bootstrapTable('getSelections').map(a => a.id); + } else { + dataId = $(e.relatedTarget).data("id"); + } + startTable(target, dataId); }); - $("#get_tags").on("click", function() { - startTable(0, 0); - $("#h1").removeClass("hidden"); - }); - $("#get_user_column_values").on("click", function() { - startTable(3, $(this).data("id")); - $("#h4").removeClass("hidden"); + // User table handling + var user_column = []; + $("#user-table > thead > tr > th").each(function() { + var element = {}; + if ($(this).attr("data-edit")) { + element = { + editable: { + mode: "inline", + emptytext: "", + error: function(response) { + return response.responseText; + } + } + }; + } + var validateText = $(this).attr("data-edit-validate"); + if (validateText) { + element.editable.validate = function (value) { + if ($.trim(value) === "") return validateText; + }; + } + user_column.push(element); }); - $("#get_user_tags").on("click", function() { - startTable(2, $(this).data("id")); - $(this)[0].blur(); - $("#h3").removeClass("hidden"); + $("#user-table").bootstrapTable({ + sidePagination: "server", + pagination: true, + paginationLoop: false, + paginationDetailHAlign: " hidden", + paginationHAlign: "left", + idField: "id", + uniqueId: "id", + search: true, + showColumns: true, + searchAlign: "left", + showSearchButton : true, + searchOnEnterKey: true, + checkboxHeader: true, + maintainMetaData: true, + responseHandler: responseHandler, + columns: user_column, + formatNoMatches: function () { + return ""; + }, + onPostBody () { + // Remove all checkboxes from Headers for showing the texts in the column selector + $('.columns [data-field]').each(function(){ + var elText = $(this).next().text(); + $(this).next().empty(); + var index = elText.lastIndexOf('\n', elText.length - 2); + if ( index > -1) { + elText = elText.substr(index); + } + $(this).next().text(elText); + }); + }, + onLoadSuccess: function () { + var guest = $(".editable[data-name='name'][data-value='Guest']"); + guest.editable("disable"); + $(".editable[data-name='locale'][data-pk='"+guest.data("pk")+"']").editable("disable"); + $("input[data-name='admin_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); + $("input[data-name='passwd_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); + $("input[data-name='edit_shelf_role'][data-pk='"+guest.data("pk")+"']").prop("disabled", true); + // ToDo: Disable delete + + }, + + // eslint-disable-next-line no-unused-vars + /*onEditableSave: function (field, row, oldvalue, $el) { + if (field === "title" || field === "authors") { + $.ajax({ + method:"get", + dataType: "json", + url: window.location.pathname + "/../../ajax/sort_value/" + field + "/" + row.id, + success: function success(data) { + var key = Object.keys(data)[0]; + $("#books-table").bootstrapTable("updateCellByUniqueId", { + id: row.id, + field: key, + value: data[key] + }); + // console.log(data); + } + }); + } + },*/ + // eslint-disable-next-line no-unused-vars + onColumnSwitch: function (field, checked) { + var visible = $("#user-table").bootstrapTable("getVisibleColumns"); + var hidden = $("#user-table").bootstrapTable("getHiddenColumns"); + var st = ""; + visible.forEach(function(item) { + st += "\"" + item.name + "\":\"" + "true" + "\","; + }); + hidden.forEach(function(item) { + st += "\"" + item.name + "\":\"" + "false" + "\","; + }); + st = st.slice(0, -1); + $.ajax({ + method:"post", + contentType: "application/json; charset=utf-8", + dataType: "json", + url: window.location.pathname + "/../../ajax/user_table_settings", + data: "{" + st + "}", + }); + }, }); + $("#user_delete_selection").click(function() { + $("#user-table").bootstrapTable("uncheckAll"); + }); + + function user_handle (userId) { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/deleteuser", + data: {"userid":userId} + }); + $.ajax({ + method:"get", + url: window.location.pathname + "/../../ajax/listusers", + async: true, + timeout: 900, + success:function(data) { + $("#user-table").bootstrapTable("load", data); + } + }); + } + + + $("#user-table").on("click-cell.bs.table", function (field, value, row, $element) { + if (value === "denied_column_value") { + ConfirmDialog("btndeluser", "GeneralDeleteModal", $element.id, user_handle); + } + }); + + $("#user-table").on("check.bs.table check-all.bs.table uncheck.bs.table uncheck-all.bs.table", + function (e, rowsAfter, rowsBefore) { + var rows = rowsAfter; + + if (e.type === "uncheck-all") { + rows = rowsBefore; + } + + var ids = $.map(!$.isArray(rows) ? [rows] : rows, function (row) { + return row.id; + }); + var func = $.inArray(e.type, ["check", "check-all"]) > -1 ? "union" : "difference"; + selections = window._[func](selections, ids); + if (selections.length < 1) { + $("#user_delete_selection").addClass("disabled"); + $("#user_delete_selection").attr("aria-disabled", true); + $(".check_head").attr("aria-disabled", true); + $(".check_head").attr("disabled", true); + $(".check_head").prop('checked', false); + $(".button_head").attr("aria-disabled", true); + $(".button_head").addClass("disabled"); + $(".header_select").attr("disabled", true); + } else { + $("#user_delete_selection").removeClass("disabled"); + $("#user_delete_selection").attr("aria-disabled", false); + $(".check_head").attr("aria-disabled", false); + $(".check_head").removeAttr("disabled"); + $(".button_head").attr("aria-disabled", false); + $(".button_head").removeClass("disabled"); + $(".header_select").removeAttr("disabled"); + } + + }); }); + /* Function for deleting domain restrictions */ function TableActions (value, row) { return [ @@ -358,7 +578,10 @@ function TableActions (value, row) { ].join(""); } - +function editEntry(param) +{ + console.log(param); +} /* Function for deleting domain restrictions */ function RestrictionActions (value, row) { return [ @@ -377,6 +600,15 @@ function EbookActions (value, row) { ].join(""); } +/* Function for deleting books */ +function UserActions (value, row) { + return [ + "
", + "", + "
" + ].join(""); +} + /* Function for keeping checked rows */ function responseHandler(res) { $.each(res.rows, function (i, row) { @@ -384,3 +616,122 @@ function responseHandler(res) { }); return res; } + +function singleUserFormatter(value, row) { + return '' + this.buttontext + '' +} + +function checkboxFormatter(value, row, index){ + if(value & this.column) + return ''; + else + return ''; +} + +function checkboxChange(checkbox, userId, field, field_index) { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/editlistusers/" + field, + data: {"pk":userId, "field_index":field_index, "value": checkbox.checked} + /*
+ +
*/ + /*
Text to show
*/ + }); + $.ajax({ + method:"get", + url: window.location.pathname + "/../../ajax/listusers", + async: true, + timeout: 900, + success:function(data) { + $("#user-table").bootstrapTable("load", data); + } + }); +} +function deactivateHeaderButtons(e) { + $("#user_delete_selection").addClass("disabled"); + $("#user_delete_selection").attr("aria-disabled", true); + $(".check_head").attr("aria-disabled", true); + $(".check_head").attr("disabled", true); + $(".check_head").prop('checked', false); + $(".button_head").attr("aria-disabled", true); + $(".button_head").addClass("disabled"); + $(".header_select").attr("disabled", true); +} + +function selectHeader(element, field) { + if (element.value !== "None") { + confirmDialog(element.id, "GeneralChangeModal", 0, function () { + var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); + $.ajax({ + method: "post", + url: window.location.pathname + "/../../ajax/editlistusers/" + field, + data: {"pk": result, "value": element.value}, + success: function () { + $.ajax({ + method: "get", + url: window.location.pathname + "/../../ajax/listusers", + async: true, + timeout: 900, + success: function (data) { + $("#user-table").bootstrapTable("load", data); + deactivateHeaderButtons(); + } + }); + } + }); + }); + } +} + +function checkboxHeader(CheckboxState, field, field_index) { + confirmDialog(field, "GeneralChangeModal", 0, function() { + var result = $('#user-table').bootstrapTable('getSelections').map(a => a.id); + $.ajax({ + method: "post", + url: window.location.pathname + "/../../ajax/editlistusers/" + field, + data: {"pk": result, "field_index": field_index, "value": CheckboxState}, + success: function () { + $.ajax({ + method: "get", + url: window.location.pathname + "/../../ajax/listusers", + async: true, + timeout: 900, + success: function (data) { + $("#user-table").bootstrapTable("load", data); + $("#user_delete_selection").addClass("disabled"); + $("#user_delete_selection").attr("aria-disabled", true); + $(".check_head").attr("aria-disabled", true); + $(".check_head").attr("disabled", true); + $(".check_head").prop('checked', false); + $(".button_head").attr("aria-disabled", true); + $(".button_head").addClass("disabled"); + $(".header_select").attr("disabled", true); + } + }); + } + }); + }); +} + +function user_handle (userId) { + $.ajax({ + method:"post", + url: window.location.pathname + "/../../ajax/deleteuser", + data: {"userid":userId} + }); + $.ajax({ + method:"get", + url: window.location.pathname + "/../../ajax/listusers", + async: true, + timeout: 900, + success:function(data) { + $("#user-table").bootstrapTable("load", data); + } + }); +} + +function test(){ + console.log("hello"); +} diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py index d3e74569..2e2a51d8 100644 --- a/cps/tasks/convert.py +++ b/cps/tasks/convert.py @@ -75,8 +75,8 @@ class TaskConvert(CalibreTask): self.settings['body'], internal=True) ) - except Exception as e: - return self._handleError(str(e)) + except Exception as ex: + return self._handleError(str(ex)) def _convert_ebook_format(self): error_message = None diff --git a/cps/tasks/mail.py b/cps/tasks/mail.py index 4d0b2f77..8d2d4188 100644 --- a/cps/tasks/mail.py +++ b/cps/tasks/mail.py @@ -4,6 +4,8 @@ import os import smtplib import threading import socket +import mimetypes +import base64 try: from StringIO import StringIO @@ -16,11 +18,14 @@ except ImportError: from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText + + from email import encoders from email.utils import formatdate, make_msgid from email.generator import Generator from cps.services.worker import CalibreTask +from cps.services import gmail from cps import logger, config from cps import gdriveutils @@ -107,68 +112,38 @@ class TaskEmail(CalibreTask): self.recipent = recipient self.text = text self.asyncSMTP = None - self.results = dict() - def run(self, worker_thread): - # create MIME message - msg = MIMEMultipart() - msg['Subject'] = self.subject - msg['Message-Id'] = make_msgid('calibre-web') - msg['Date'] = formatdate(localtime=True) + def prepare_message(self): + message = MIMEMultipart() + message['to'] = self.recipent + message['from'] = self.settings["mail_from"] + message['subject'] = self.subject + message['Message-Id'] = make_msgid('calibre-web') + message['Date'] = formatdate(localtime=True) text = self.text - msg.attach(MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8')) + msg = MIMEText(text.encode('UTF-8'), 'plain', 'UTF-8') + message.attach(msg) if self.attachment: result = self._get_attachment(self.filepath, self.attachment) if result: - msg.attach(result) + message.attach(result) else: self._handleError(u"Attachment not found") return + return message - msg['From'] = self.settings["mail_from"] - msg['To'] = self.recipent - - use_ssl = int(self.settings.get('mail_use_ssl', 0)) + def run(self, worker_thread): + # create MIME message + msg = self.prepare_message() try: - # convert MIME message to string - fp = StringIO() - gen = Generator(fp, mangle_from_=False) - gen.flatten(msg) - msg = fp.getvalue() - - # send email - timeout = 600 # set timeout to 5mins - - # redirect output to logfile on python2 pn python3 debugoutput is caught with overwritten - # _print_debug function - if sys.version_info < (3, 0): - org_smtpstderr = smtplib.stderr - smtplib.stderr = logger.StderrLogger('worker.smtp') - - if use_ssl == 2: - self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"], - timeout=timeout) + if self.settings['mail_server_type'] == 0: + self.send_standard_email(msg) else: - self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout) - - # link to logginglevel - if logger.is_debug_enabled(): - self.asyncSMTP.set_debuglevel(1) - if use_ssl == 1: - self.asyncSMTP.starttls() - if self.settings["mail_password"]: - self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"])) - self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, msg) - self.asyncSMTP.quit() - self._handleSuccess() - - if sys.version_info < (3, 0): - smtplib.stderr = org_smtpstderr - - except (MemoryError) as e: + self.send_gmail_email(msg) + except MemoryError as e: log.debug_or_exception(e) - self._handleError(u'MemoryError sending email: ' + str(e)) + self._handleError(u'MemoryError sending email: {}'.format(str(e))) except (smtplib.SMTPException, smtplib.SMTPAuthenticationError) as e: log.debug_or_exception(e) if hasattr(e, "smtp_error"): @@ -179,12 +154,55 @@ class TaskEmail(CalibreTask): text = '\n'.join(e.args) else: text = '' - self._handleError(u'Smtplib Error sending email: ' + text) - except (socket.error) as e: + self._handleError(u'Smtplib Error sending email: {}'.format(text)) + except socket.error as e: log.debug_or_exception(e) - self._handleError(u'Socket Error sending email: ' + e.strerror) + self._handleError(u'Socket Error sending email: {}'.format(e.strerror)) + except Exception as ex: + log.debug_or_exception(ex) + self._handleError(u'Error sending email: {}'.format(ex)) + def send_standard_email(self, msg): + use_ssl = int(self.settings.get('mail_use_ssl', 0)) + timeout = 600 # set timeout to 5mins + + # redirect output to logfile on python2 on python3 debugoutput is caught with overwritten + # _print_debug function + if sys.version_info < (3, 0): + org_smtpstderr = smtplib.stderr + smtplib.stderr = logger.StderrLogger('worker.smtp') + + if use_ssl == 2: + self.asyncSMTP = EmailSSL(self.settings["mail_server"], self.settings["mail_port"], + timeout=timeout) + else: + self.asyncSMTP = Email(self.settings["mail_server"], self.settings["mail_port"], timeout=timeout) + + # link to logginglevel + if logger.is_debug_enabled(): + self.asyncSMTP.set_debuglevel(1) + if use_ssl == 1: + self.asyncSMTP.starttls() + if self.settings["mail_password"]: + self.asyncSMTP.login(str(self.settings["mail_login"]), str(self.settings["mail_password"])) + + # Convert message to something to send + fp = StringIO() + gen = Generator(fp, mangle_from_=False) + gen.flatten(msg) + + self.asyncSMTP.sendmail(self.settings["mail_from"], self.recipent, fp.getvalue()) + self.asyncSMTP.quit() + self._handleSuccess() + + if sys.version_info < (3, 0): + smtplib.stderr = org_smtpstderr + + + def send_gmail_email(self, message): + return gmail.send_messsage(self.settings.get('mail_gmail_token', None), message) + @property def progress(self): if self.asyncSMTP is not None: @@ -203,13 +221,13 @@ class TaskEmail(CalibreTask): @classmethod def _get_attachment(cls, bookpath, filename): """Get file as MIMEBase message""" - calibrepath = config.config_calibre_dir + calibre_path = config.config_calibre_dir if config.config_use_google_drive: df = gdriveutils.getFileFromEbooksFolder(bookpath, filename) if df: - datafile = os.path.join(calibrepath, bookpath, filename) - if not os.path.exists(os.path.join(calibrepath, bookpath)): - os.makedirs(os.path.join(calibrepath, bookpath)) + datafile = os.path.join(calibre_path, bookpath, filename) + if not os.path.exists(os.path.join(calibre_path, bookpath)): + os.makedirs(os.path.join(calibre_path, bookpath)) df.GetContentFile(datafile) else: return None @@ -219,19 +237,22 @@ class TaskEmail(CalibreTask): os.remove(datafile) else: try: - file_ = open(os.path.join(calibrepath, bookpath, filename), 'rb') + file_ = open(os.path.join(calibre_path, bookpath, filename), 'rb') data = file_.read() file_.close() except IOError as e: log.debug_or_exception(e) log.error(u'The requested file could not be read. Maybe wrong permissions?') return None - - attachment = MIMEBase('application', 'octet-stream') + # Set mimetype + content_type, encoding = mimetypes.guess_type(filename) + if content_type is None or encoding is not None: + content_type = 'application/octet-stream' + main_type, sub_type = content_type.split('/', 1) + attachment = MIMEBase(main_type, sub_type) attachment.set_payload(data) encoders.encode_base64(attachment) - attachment.add_header('Content-Disposition', 'attachment', - filename=filename) + attachment.add_header('Content-Disposition', 'attachment', filename=filename) return attachment @property diff --git a/cps/templates/admin.html b/cps/templates/admin.html index e762c0c2..5226151e 100644 --- a/cps/templates/admin.html +++ b/cps/templates/admin.html @@ -7,6 +7,7 @@

{{_('Users')}}

+ {% if allUser.__len__() < 10 %} @@ -25,7 +26,7 @@ {% for user in allUser %} {% if not user.role_anonymous() or config.config_anonbrowse %} - + @@ -41,6 +42,8 @@ {% endif %} {% endfor %}
{{_('Username')}}
{{user.nickname}}{{user.name}} {{user.email}} {{user.kindle_mail}} {{user.downloads.count()}}
+ {% endif %} + {{_('Edit Users')}} {{_('Add New User')}} {% if (config.config_login_type == 1) %}
{{_('Import LDAP Users')}}
@@ -52,29 +55,42 @@

{{_('E-mail Server Settings')}}

{% if config.get_mail_server_configured() %} -
-
+ {% if email.mail_server_type == 0 %} +
+
{{_('SMTP Hostname')}}
{{email.mail_server}}
-
-
+
+
{{_('SMTP Port')}}
{{email.mail_port}}
-
-
+
+
{{_('Encryption')}}
{{ display_bool_setting(email.mail_use_ssl) }}
+
+
+
{{_('SMTP Login')}}
+
{{email.mail_login}}
+
+
+
{{_('From E-mail')}}
+
{{email.mail_from}}
+
-
-
{{_('SMTP Login')}}
-
{{email.mail_login}}
+ {% else %} +
+
+
{{_('E-Mail Service')}}
+
{{_('Gmail via Oauth2')}}
+
+
+
{{_('From E-mail')}}
+
{{email.mail_gmail_token['email']}}
+
-
-
{{_('From E-mail')}}
-
{{email.mail_from}}
-
-
{% endif %} + {% endif %} {{_('Edit E-mail Server Settings')}}
diff --git a/cps/templates/book_table.html b/cps/templates/book_table.html index ecd840b5..6a31c235 100644 --- a/cps/templates/book_table.html +++ b/cps/templates/book_table.html @@ -30,8 +30,8 @@
- - + +
@@ -53,7 +53,7 @@ {{ text_table_row('languages', _('Enter Languages'),_('Languages'), false) }} {{ text_table_row('publishers', _('Enter Publishers'),_('Publishers'), false) }} - {% if g.user.role_edit() %} + {% if g.user.role_delete_books() and g.user.role_edit()%} {{_('Delete')}} {% endif %} @@ -94,6 +94,4 @@ - {% endblock %} diff --git a/cps/templates/config_view_edit.html b/cps/templates/config_view_edit.html index d87e9f81..e0a8094c 100644 --- a/cps/templates/config_view_edit.html +++ b/cps/templates/config_view_edit.html @@ -141,8 +141,8 @@
- {{_('Add Allowed/Denied Tags')}} - {{_('Add Allowed/Denied custom column values')}} + {{_('Add Allowed/Denied Tags')}} + {{_('Add Allowed/Denied custom column values')}} diff --git a/cps/templates/email_edit.html b/cps/templates/email_edit.html index 0dd9ef17..8b92a248 100644 --- a/cps/templates/email_edit.html +++ b/cps/templates/email_edit.html @@ -7,44 +7,66 @@

{{title}}

+ {% if feature_support['gmail'] %}
- - + +
-
- - +
+
+ {% if content.mail_gmail_token == {} %} + + {% else %} + + {% endif %} +
-
- - +
+ {% endif %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + + +
+ + + {% if feature_support['gmail'] %}
-
- - -
-
- - -
-
- - -
- -
- - - - -
- - - {{_('Cancel')}} + {% endif %} + {{_('Back')}} {% if g.allow_registration %}
diff --git a/cps/templates/feed.xml b/cps/templates/feed.xml index 4ad1db8c..9073142e 100644 --- a/cps/templates/feed.xml +++ b/cps/templates/feed.xml @@ -84,4 +84,11 @@ {% endfor %} + {% for entry in letterelements %} + + {{entry['name']}} + {{ url_for(folder, book_id=entry['id']) }} + + + {% endfor %} diff --git a/cps/templates/grid.html b/cps/templates/grid.html index 9724e31d..6b03d89c 100644 --- a/cps/templates/grid.html +++ b/cps/templates/grid.html @@ -8,8 +8,8 @@ {% endif %} {% endif %} - - + + {% if charlist|length %} {% endif %} diff --git a/cps/templates/index.xml b/cps/templates/index.xml index c6a6e8f0..4ffd4290 100644 --- a/cps/templates/index.xml +++ b/cps/templates/index.xml @@ -14,6 +14,13 @@ {{instance}} https://github.com/janeczku/calibre-web + + {{_('Alphabetical Books')}} + + {{url_for('opds.feed_booksindex')}} + {{ current_time }} + {{_('Books sorted alphabetically')}} + {{_('Hot Books')}} diff --git a/cps/templates/layout.html b/cps/templates/layout.html index 643459cd..d4e0800e 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -1,4 +1,4 @@ -{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal %} +{% from 'modal_dialogs.html' import restrict_modal, delete_book, filechooser_modal, delete_confirm_modal, change_confirm_modal %} @@ -76,7 +76,7 @@ {% if g.user.role_admin() %}
  • {% endif %} -
  • +
  • {% if not g.user.is_anonymous %}
  • {% endif %} diff --git a/cps/templates/list.html b/cps/templates/list.html index 53a712d1..1e171ca5 100644 --- a/cps/templates/list.html +++ b/cps/templates/list.html @@ -8,8 +8,8 @@ {% endif %} {% endif %} - - + + {% if charlist|length %} {% endif %} diff --git a/cps/templates/modal_dialogs.html b/cps/templates/modal_dialogs.html index da00834c..e134236e 100644 --- a/cps/templates/modal_dialogs.html +++ b/cps/templates/modal_dialogs.html @@ -22,7 +22,7 @@
    - +
    @@ -108,13 +108,31 @@ +
    +{% endmacro %} + +{% macro change_confirm_modal() %} +