Fix confirm dialog database change

Gdrive setup basically working again
Moved basicconfig behind login
Database setup separated from other setup
Config page is using ajax (flask >2 and slow computers)
This commit is contained in:
Ozzie Isaacs 2021-05-26 13:35:35 +02:00
parent dcdb5e2a9e
commit a47d6cd937
16 changed files with 2402 additions and 725 deletions

View File

@ -83,7 +83,9 @@ log = logger.create()
from . import services
db.CalibreDB.setup_db(config, cli.settingspath)
db.CalibreDB.update_config(config)
db.CalibreDB.setup_db(config.config_calibre_dir, cli.settingspath)
calibre_db = db.CalibreDB()

View File

@ -40,7 +40,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.sql.expression import func, or_, text
from . import constants, logger, helper, services
from .cli import filepicker
# 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, check_email, \
valid_email, check_username
@ -97,19 +97,6 @@ def admin_required(f):
return inner
def unconfigured(f):
"""
Checks if calibre-web instance is not configured
"""
@wraps(f)
def inner(*args, **kwargs):
if not config.db_configured:
return f(*args, **kwargs)
abort(403)
return inner
@admi.before_app_request
def before_request():
if current_user.is_authenticated:
@ -124,10 +111,14 @@ def before_request():
g.shelves_access = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
if '/static/' not in request.path and not config.db_configured and \
request.endpoint not in ('admin.basic_configuration',
'login',
'admin.config_pathchooser'):
return redirect(url_for('admin.basic_configuration'))
request.endpoint not in ('admin.ajax_db_config',
'admin.simulatedbchange',
'admin.db_configuration',
'web.login',
'web.logout',
'admin.load_dialogtexts',
'admin.ajax_pathchooser'):
return redirect(url_for('admin.db_configuration'))
@admi.route("/admin")
@ -194,16 +185,46 @@ def admin():
feature_support=feature_support, kobo_support=kobo_support,
title=_(u"Admin page"), page="admin")
@admi.route("/admin/dbconfig", methods=["GET", "POST"])
@login_required
@admin_required
def db_configuration():
if request.method == "POST":
return _db_configuration_update_helper()
return _db_configuration_result()
@admi.route("/admin/config", methods=["GET", "POST"])
@admi.route("/admin/config", methods=["GET"])
@login_required
@admin_required
def configuration():
if request.method == "POST":
return _configuration_update_helper(True)
return _configuration_result()
return render_title_template("config_edit.html",
config=config,
provider=oauthblueprints,
feature_support=feature_support,
title=_(u"Basic Configuration"), page="config")
@admi.route("/admin/ajaxconfig", methods=["POST"])
@login_required
@admin_required
def ajax_config():
return _configuration_update_helper()
@admi.route("/admin/ajaxdbconfig", methods=["POST"])
@login_required
@admin_required
def ajax_db_config():
return _db_configuration_update_helper()
@admi.route("/admin/alive", methods=["GET"])
@login_required
@admin_required
def calibreweb_alive():
return "", 200
@admi.route("/admin/viewconfig")
@login_required
@admin_required
@ -539,10 +560,10 @@ def update_view_configuration():
return view_configuration()
@admi.route("/ajax/loaddialogtexts/<element_id>")
@admi.route("/ajax/loaddialogtexts/<element_id>", methods=['POST'])
@login_required
def load_dialogtexts(element_id):
texts = {"header": "", "main": ""}
texts = {"header": "", "main": "", "valid": 1}
if element_id == "config_delete_kobo_token":
texts["main"] = _('Do you really want to delete the Kobo Token?')
elif element_id == "btndeletedomain":
@ -563,6 +584,8 @@ def load_dialogtexts(element_id):
texts["main"] = _('Are you sure you want to change the selected visibility restrictions for the selected user(s)?')
elif element_id == "kobo_only_shelves_sync":
texts["main"] = _('Are you sure you want to change shelf sync behavior for the selected user(s)?')
elif element_id == "db_submit":
texts["main"] = _('Are you sure you want to change Calibre libray location?')
return json.dumps(texts)
@ -867,14 +890,6 @@ def list_restriction(res_type, user_id):
return response
@admi.route("/basicconfig/pathchooser/")
@unconfigured
def config_pathchooser():
if filepicker:
return pathchooser()
abort(403)
@admi.route("/ajax/pathchooser/")
@login_required
@admin_required
@ -963,16 +978,6 @@ def pathchooser():
return json.dumps(context)
@admi.route("/basicconfig", methods=["GET", "POST"])
@unconfigured
def basic_configuration():
logout_user()
if request.method == "POST":
log.debug("Basic Configuration send")
return _configuration_update_helper(configured=filepicker)
return _configuration_result(configured=filepicker)
def _config_int(to_save, x, func=int):
return config.set_from_dictionary(to_save, x, func)
@ -991,23 +996,24 @@ def _config_string(to_save, x):
def _configuration_gdrive_helper(to_save):
gdrive_error = None
gdrive_secrets = {}
if to_save.get("config_use_google_drive"):
gdrive_secrets = {}
if not os.path.isfile(gdriveutils.SETTINGS_YAML):
config.config_use_google_drive = False
if not os.path.isfile(gdriveutils.SETTINGS_YAML):
config.config_use_google_drive = False
if gdrive_support:
gdrive_error = gdriveutils.get_error_text(gdrive_secrets)
if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error:
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
gdrive_secrets = json.load(settings)['web']
if not gdrive_secrets:
return _configuration_result(_('client_secrets.json Is Not Configured For Web Application'))
gdriveutils.update_settings(
gdrive_secrets['client_id'],
gdrive_secrets['client_secret'],
gdrive_secrets['redirect_uris'][0]
)
if gdrive_support:
gdrive_error = gdriveutils.get_error_text(gdrive_secrets)
if "config_use_google_drive" in to_save and not config.config_use_google_drive and not gdrive_error:
with open(gdriveutils.CLIENT_SECRETS, 'r') as settings:
gdrive_secrets = json.load(settings)['web']
if not gdrive_secrets:
return _configuration_result(_('client_secrets.json Is Not Configured For Web Application'))
gdriveutils.update_settings(
gdrive_secrets['client_id'],
gdrive_secrets['client_secret'],
gdrive_secrets['redirect_uris'][0]
)
# always show google drive settings, but in case of error deny support
new_gdrive_value = (not gdrive_error) and ("config_use_google_drive" in to_save)
@ -1041,23 +1047,23 @@ def _configuration_oauth_helper(to_save):
return reboot_required
def _configuration_logfile_helper(to_save, gdrive_error):
def _configuration_logfile_helper(to_save):
reboot_required = False
reboot_required |= _config_int(to_save, "config_log_level")
reboot_required |= _config_string(to_save, "config_logfile")
if not logger.is_valid_logfile(config.config_logfile):
return reboot_required, \
_configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error)
_configuration_result(_('Logfile Location is not Valid, Please Enter Correct Path'))
reboot_required |= _config_checkbox_int(to_save, "config_access_log")
reboot_required |= _config_string(to_save, "config_access_logfile")
if not logger.is_valid_logfile(config.config_access_logfile):
return reboot_required, \
_configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'), gdrive_error)
_configuration_result(_('Access Logfile Location is not Valid, Please Enter Correct Path'))
return reboot_required, None
def _configuration_ldap_helper(to_save, gdrive_error):
def _configuration_ldap_helper(to_save):
reboot_required = False
reboot_required |= _config_string(to_save, "config_ldap_provider_url")
reboot_required |= _config_int(to_save, "config_ldap_port")
@ -1084,44 +1090,37 @@ def _configuration_ldap_helper(to_save, gdrive_error):
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)
'Port, DN and User Object Identifier'))
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):
return reboot_required, _configuration_result('Please Enter a LDAP Service Account and Password',
gdrive_error)
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account and Password'))
else:
if not config.config_ldap_serv_username:
return reboot_required, _configuration_result('Please Enter a LDAP Service Account', gdrive_error)
return reboot_required, _configuration_result(_('Please Enter a LDAP Service Account'))
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)
_configuration_result(_('LDAP Group Object Filter Needs to Have One "%s" Format Identifier'))
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)
return reboot_required, _configuration_result(_('LDAP Group Object Filter Has Unmatched Parenthesis'))
if config.config_ldap_user_object.count("%s") != 1:
return reboot_required, \
_configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'),
gdrive_error)
_configuration_result(_('LDAP User Object Filter needs to Have One "%s" Format Identifier'))
if config.config_ldap_user_object.count("(") != config.config_ldap_user_object.count(")"):
return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'),
gdrive_error)
return reboot_required, _configuration_result(_('LDAP User Object Filter Has Unmatched Parenthesis'))
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:
return reboot_required, \
_configuration_result(_('LDAP Member User Filter needs to Have One "%s" Format Identifier'),
gdrive_error)
_configuration_result(_('LDAP Member User Filter needs to Have One "%s" Format Identifier'))
if config.config_ldap_member_user_object.count("(") != config.config_ldap_member_user_object.count(")"):
return reboot_required, _configuration_result(_('LDAP Member User Filter Has Unmatched Parenthesis'),
gdrive_error)
return reboot_required, _configuration_result(_('LDAP Member User Filter Has Unmatched Parenthesis'))
if config.config_ldap_cacert_path or config.config_ldap_cert_path or config.config_ldap_key_path:
if not (os.path.isfile(config.config_ldap_cacert_path) and
@ -1129,13 +1128,31 @@ def _configuration_ldap_helper(to_save, gdrive_error):
os.path.isfile(config.config_ldap_key_path)):
return reboot_required, \
_configuration_result(_('LDAP CACertificate, Certificate or Key Location is not Valid, '
'Please Enter Correct Path'),
gdrive_error)
'Please Enter Correct Path'))
return reboot_required, None
def _configuration_update_helper(configured):
reboot_required = False
@admi.route("/ajax/simulatedbchange", methods=['POST'])
@login_required
@admin_required
def simulatedbchange():
db_change, db_valid = _db_simulate_change()
return Response(json.dumps({"change": db_change, "valid": db_valid}), mimetype='application/json')
def _db_simulate_change():
param = request.form.to_dict()
to_save = {}
to_save['config_calibre_dir'] = re.sub(r'[\\/]metadata\.db$',
'',
param['config_calibre_dir'],
flags=re.IGNORECASE).strip()
db_change = config.config_calibre_dir != to_save["config_calibre_dir"] and config.config_calibre_dir
db_valid = calibre_db.check_valid_db(to_save["config_calibre_dir"], ub.app_DB_path)
return db_change, db_valid
def _db_configuration_update_helper():
db_change = False
to_save = request.form.to_dict()
gdrive_error = None
@ -1145,24 +1162,47 @@ def _configuration_update_helper(configured):
to_save['config_calibre_dir'],
flags=re.IGNORECASE)
try:
db_change |= _config_string(to_save, "config_calibre_dir")
db_change, db_valid = _db_simulate_change()
# gdrive_error drive setup
gdrive_error = _configuration_gdrive_helper(to_save)
except (OperationalError, InvalidRequestError):
ub.session.rollback()
log.error("Settings DB is not Writeable")
_db_configuration_result(_("Settings DB is not Writeable"), gdrive_error)
try:
metadata_db = os.path.join(to_save['config_calibre_dir'], "metadata.db")
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 ex:
return _db_configuration_result('{}'.format(ex), gdrive_error)
if db_change or not db_valid or not config.db_configured:
if not calibre_db.setup_db(to_save['config_calibre_dir'], ub.app_DB_path):
return _db_configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
gdrive_error)
_config_string(to_save, "config_calibre_dir")
calibre_db.update_config(config)
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
flash(_(u"DB is not Writeable"), category="warning")
# warning = {'type': "warning", 'message': _(u"DB is not Writeable")}
config.save()
return _db_configuration_result(None, gdrive_error)
def _configuration_update_helper():
reboot_required = False
to_save = request.form.to_dict()
try:
reboot_required |= _config_int(to_save, "config_port")
reboot_required |= _config_string(to_save, "config_keyfile")
if config.config_keyfile and not os.path.isfile(config.config_keyfile):
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'),
gdrive_error,
configured)
return _configuration_result(_('Keyfile Location is not Valid, Please Enter Correct Path'))
reboot_required |= _config_string(to_save, "config_certfile")
if config.config_certfile and not os.path.isfile(config.config_certfile):
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'),
gdrive_error,
configured)
return _configuration_result(_('Certfile Location is not Valid, Please Enter Correct Path'))
_config_checkbox_int(to_save, "config_uploading")
# Reboot on config_anonbrowse with enabled ldap, as decoraters are changed in this case
@ -1186,15 +1226,14 @@ def _configuration_update_helper(configured):
reboot_required |= _config_int(to_save, "config_login_type")
# LDAP configurator,
# LDAP configurator
if config.config_login_type == constants.LOGIN_LDAP:
reboot, message = _configuration_ldap_helper(to_save, gdrive_error)
reboot, message = _configuration_ldap_helper(to_save)
if message:
return message
reboot_required |= reboot
# Remote login configuration
_config_checkbox(to_save, "config_remote_login")
if not config.config_remote_login:
ub.session.query(ub.RemoteAuthToken).filter(ub.RemoteAuthToken.token_type == 0).delete()
@ -1218,7 +1257,7 @@ def _configuration_update_helper(configured):
if config.config_login_type == constants.LOGIN_OAUTH:
reboot_required |= _configuration_oauth_helper(to_save)
reboot, message = _configuration_logfile_helper(to_save, gdrive_error)
reboot, message = _configuration_logfile_helper(to_save)
if message:
return message
reboot_required |= reboot
@ -1227,67 +1266,55 @@ def _configuration_update_helper(configured):
if "config_rarfile_location" in to_save:
unrar_status = helper.check_unrar(config.config_rarfile_location)
if unrar_status:
return _configuration_result(unrar_status, gdrive_error, configured)
return _configuration_result(unrar_status)
except (OperationalError, InvalidRequestError):
ub.session.rollback()
log.error("Settings DB is not Writeable")
_configuration_result(_("Settings DB is not Writeable"), gdrive_error, configured)
try:
metadata_db = os.path.join(config.config_calibre_dir, "metadata.db")
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 ex:
return _configuration_result('%s' % ex, gdrive_error, configured)
if db_change:
if not calibre_db.setup_db(config, ub.app_DB_path):
return _configuration_result(_('DB Location is not Valid, Please Enter Correct Path'),
gdrive_error,
configured)
if not os.access(os.path.join(config.config_calibre_dir, "metadata.db"), os.W_OK):
flash(_(u"DB is not Writeable"), category="warning")
_configuration_result(_("Settings DB is not Writeable"))
config.save()
flash(_(u"Calibre-Web configuration updated"), category="success")
if reboot_required:
web_server.stop(True)
return _configuration_result(None, gdrive_error, configured)
return _configuration_result(None, reboot_required)
def _configuration_result(error_flash=None, reboot=False):
resp = {}
if error_flash:
log.error(error_flash)
config.load()
resp['result'] = [{'type': "danger", 'message': error_flash}]
else:
resp['result'] = [{'type': "success", 'message':_(u"Calibre-Web configuration updated")}]
resp['reboot'] = reboot
resp['config_upload']= config.config_upload_formats
return Response(json.dumps(resp), mimetype='application/json')
def _configuration_result(error_flash=None, gdrive_error=None, configured=True):
def _db_configuration_result(error_flash=None, gdrive_error=None):
gdrive_authenticate = not is_gdrive_ready()
gdrivefolders = []
if gdrive_error is None:
if not gdrive_error and config.config_use_google_drive:
gdrive_error = gdriveutils.get_error_text()
if gdrive_error and gdrive_support:
log.error(gdrive_error)
gdrive_error = _(gdrive_error)
flash(gdrive_error, category="error")
else:
if not gdrive_authenticate and gdrive_support:
gdrivefolders = gdriveutils.listRootFolders()
show_back_button = current_user.is_authenticated
show_login_button = config.db_configured and not current_user.is_authenticated
if error_flash:
log.error(error_flash)
config.load()
flash(error_flash, category="error")
show_login_button = False
return render_title_template("config_edit.html",
return render_title_template("config_db.html",
config=config,
provider=oauthblueprints,
show_back_button=show_back_button,
show_login_button=show_login_button,
show_authenticate_google_drive=gdrive_authenticate,
filepicker=configured,
gdriveError=gdrive_error,
gdrivefolders=gdrivefolders,
feature_support=feature_support,
title=_(u"Basic Configuration"), page="config")
title=_(u"Database Configuration"), page="dbconfig")
def _handle_new_user(to_save, content, languages, translations, kobo_support):

View File

@ -45,7 +45,6 @@ parser.add_argument('-v', '--version', action='version', help='Shows version num
version=version_info())
parser.add_argument('-i', metavar='ip-address', help='Server IP-Address to listen')
parser.add_argument('-s', metavar='user:pass', help='Sets specific username to new password')
parser.add_argument('-f', action='store_true', help='Enables filepicker in unconfigured mode')
args = parser.parse_args()
if sys.version_info < (3, 0):
@ -114,6 +113,3 @@ user_credentials = args.s or None
if user_credentials and ":" not in user_credentials:
print("No valid 'username:password' format")
sys.exit(3)
# Handles enabling of filepicker
filepicker = args.f or None

View File

@ -347,7 +347,7 @@ class _ConfigSQL(object):
log.error(error)
log.warning("invalidating configuration")
self.db_configured = False
self.config_calibre_dir = None
# self.config_calibre_dir = None
self.save()

View File

@ -524,19 +524,44 @@ class CalibreDB():
return cc_classes
@classmethod
def setup_db(cls, config, app_db_path):
def check_valid_db(cls, config_calibre_dir, app_db_path):
if not config_calibre_dir:
return False
dbpath = os.path.join(config_calibre_dir, "metadata.db")
if not os.path.exists(dbpath):
return False
try:
check_engine = create_engine('sqlite://',
echo=False,
isolation_level="SERIALIZABLE",
connect_args={'check_same_thread': False},
poolclass=StaticPool)
with check_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)))
check_engine.connect()
except Exception:
return False
return True
@classmethod
def update_config(cls, config):
cls.config = config
@classmethod
def setup_db(cls, config_calibre_dir, app_db_path):
# cls.config = config
cls.dispose()
# toDo: if db changed -> delete shelfs, delete download books, delete read boks, kobo sync??
if not config.config_calibre_dir:
config.invalidate()
if not config_calibre_dir:
cls.config.invalidate()
return False
dbpath = os.path.join(config.config_calibre_dir, "metadata.db")
dbpath = os.path.join(config_calibre_dir, "metadata.db")
if not os.path.exists(dbpath):
config.invalidate()
cls.config.invalidate()
return False
try:
@ -552,10 +577,10 @@ class CalibreDB():
conn = cls.engine.connect()
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
except Exception as ex:
config.invalidate(ex)
cls.config.invalidate(ex)
return False
config.db_configured = True
cls.config.db_configured = True
if not cc_classes:
try:
@ -828,7 +853,8 @@ class CalibreDB():
def reconnect_db(self, config, app_db_path):
self.dispose()
self.engine.dispose()
self.setup_db(config, app_db_path)
self.setup_db(config.config_calibre_dir, app_db_path)
self.update_config(config)
def lcase(s):

View File

@ -74,7 +74,7 @@ def google_drive_callback():
f.write(credentials.to_json())
except (ValueError, AttributeError) as error:
log.error(error)
return redirect(url_for('admin.configuration'))
return redirect(url_for('admin.db_configuration'))
@gdrive.route("/watch/subscribe")
@ -99,7 +99,7 @@ def watch_gdrive():
else:
flash(reason['message'], category="error")
return redirect(url_for('admin.configuration'))
return redirect(url_for('admin.db_configuration'))
@gdrive.route("/watch/revoke")
@ -115,7 +115,7 @@ def revoke_watch_gdrive():
pass
config.config_google_drive_watch_changes_response = {}
config.save()
return redirect(url_for('admin.configuration'))
return redirect(url_for('admin.db_configuration'))
@gdrive.route("/watch/callback", methods=['GET', 'POST'])

View File

@ -141,7 +141,7 @@ function confirmDialog(id, dialogid, dataValue, yesFn, noFn) {
$confirm.modal("hide");
});
$.ajax({
method:"get",
method:"post",
dataType: "json",
url: getPath() + "/ajax/loaddialogtexts/" + id,
success: function success(data) {
@ -179,18 +179,6 @@ $("#delete_confirm").click(function() {
}
});
$("#books-table").bootstrapTable("refresh");
/*$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/listbooks",
async: true,
timeout: 900,
success:function(data) {
$("#book-table").bootstrapTable("load", data);
loadSuccess();
}
});*/
}
});
} else {
@ -218,8 +206,6 @@ $("#deleteModal").on("show.bs.modal", function(e) {
$(e.currentTarget).find("#delete_confirm").data("ajax", $(e.relatedTarget).data("ajax"));
});
$(function() {
var updateTimerID;
var updateText;
@ -556,6 +542,86 @@ $(function() {
this.closest("form").submit();
});
function handle_response(data) {
if (!jQuery.isEmptyObject(data)) {
data.forEach(function (item) {
$(".navbar").after('<div class="row-fluid text-center" style="margin-top: -20px;">' +
'<div id="flash_' + item.type + '" class="alert alert-' + item.type + '">' + item.message + '</div>' +
'</div>');
});
}
}
$('.collapse').on('shown.bs.collapse', function(){
$(this).parent().find(".glyphicon-plus").removeClass("glyphicon-plus").addClass("glyphicon-minus");
}).on('hidden.bs.collapse', function(){
$(this).parent().find(".glyphicon-minus").removeClass("glyphicon-minus").addClass("glyphicon-plus");
});
function changeDbSettings() {
$("#db_submit").closest('form').submit();
}
$("#db_submit").click(function(e) {
e.preventDefault();
e.stopPropagation();
this.blur();
$.ajax({
method:"post",
dataType: "json",
url: window.location.pathname + "/../../ajax/simulatedbchange",
data: {config_calibre_dir: $("#config_calibre_dir").val()},
success: function success(data) {
if ( data.change ) {
if ( data.valid ) {
confirmDialog(
"db_submit",
"GeneralChangeModal",
0,
changeDbSettings
);
}
else {
$("#InvalidDialog").modal('show');
}
} else {
changeDbSettings();
}
}
});
});
$("#config_submit").click(function(e) {
e.preventDefault();
e.stopPropagation();
this.blur();
window.scrollTo({top: 0, behavior: 'smooth'});
var request_path = "/../../admin/ajaxconfig";
var loader = "/../..";
$("#flash_success").remove();
$("#flash_danger").remove();
$.post(window.location.pathname + request_path, $(this).closest("form").serialize(), function(data) {
$('#config_upload_formats').val(data.config_upload);
if(data.reboot) {
$("#spinning_success").show();
var rebootInterval = setInterval(function(){
$.get({
url:window.location.pathname + "/../../admin/alive",
success: function (d, statusText, xhr) {
if (xhr.status < 400) {
$("#spinning_success").hide();
clearInterval(rebootInterval);
handle_response(data.result);
}
},
});
}, 1000);
} else {
handle_response(data.result);
}
});
});
$("#delete_shelf").click(function() {
confirmDialog(
$(this).attr('id'),
@ -568,7 +634,6 @@ $(function() {
});
$("#fileModal").on("show.bs.modal", function(e) {
var target = $(e.relatedTarget);
var path = $("#" + target.data("link"))[0].value;
@ -632,7 +697,6 @@ $(function() {
$(".update-view").click(function(e) {
var view = $(this).data("view");
e.preventDefault();
e.stopPropagation();
$.ajax({

View File

@ -150,6 +150,7 @@
</div>
{% endif %}
</div>
<a class="btn btn-default" id="db_config" href="{{url_for('admin.db_configuration')}}">{{_('Edit Calibre Database Configuration')}}</a>
<a class="btn btn-default" id="basic_config" href="{{url_for('admin.configuration')}}">{{_('Edit Basic Configuration')}}</a>
<a class="btn btn-default" id="view_config" href="{{url_for('admin.view_configuration')}}">{{_('Edit UI Configuration')}}</a>
</div>

View File

@ -0,0 +1,74 @@
{% extends "layout.html" %}
{% block flash %}
<div id="spinning_success" class="row-fluid text-center" style="margin-top: -20px; display:none;">
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
</div>
{% endblock %}
{% block body %}
<div class="discover">
<h2>{{title}}</h2>
<form role="form" method="POST" class="col-md-10 col-lg-6" action="{{ url_for('admin.db_configuration') }}" autocomplete="off">
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
<div class="form-group required input-group">
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
<span class="input-group-btn">
<button type="button" data-toggle="modal" id="calibre_modal_path" data-link="config_calibre_dir" data-filefilter="metadata.db" data-target="#fileModal" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
</div>
{% if feature_support['gdrive'] %}
<div class="form-group required">
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >
<label for="config_use_google_drive">{{_('Use Google Drive?')}}</label>
</div>
{% if not gdriveError %}
{% if show_authenticate_google_drive and config.config_use_google_drive %}
<div class="form-group required">
<a href="{{ url_for('gdrive.authenticate_google_drive') }}" id="gdrive_auth" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a>
</div>
{% else %}
{% if not show_authenticate_google_drive %}
<div class="form-group required">
<label for="config_google_drive_folder">{{_('Google Drive Calibre folder')}}</label>
<select name="config_google_drive_folder" id="config_google_drive_folder" class="form-control">
{% for gdrivefolder in gdrivefolders %}
<option value="{{ gdrivefolder.title }}" {% if gdrivefolder.title == config.config_google_drive_folder %}selected{% endif %}>{{ gdrivefolder.title }}</option>
{% endfor %}
</select>
</div>
{% if config.config_google_drive_watch_changes_response %}
<label for="config_google_drive_watch_changes_response">{{_('Metadata Watch Channel ID')}}</label>
<div class="form-group input-group required">
<input type="text" class="form-control" name="config_google_drive_watch_changes_response" id="config_google_drive_watch_changes_response" value="{{ config.config_google_drive_watch_changes_response['id'] }} expires on {{ config.config_google_drive_watch_changes_response['expiration'] | strftime }}" autocomplete="off" disabled="">
<span class="input-group-btn"><a href="{{ url_for('gdrive.revoke_watch_gdrive') }}" id="watch_revoke" class="btn btn-primary">{{_('Revoke')}}</a></span>
</div>
{% else %}
<a href="{{ url_for('gdrive.watch_gdrive') }}" id="enable_gdrive_watch" class="btn btn-primary">Enable watch of metadata.db</a>
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
<div class="col-sm-12">
<div id="db_submit" name="submit" class="btn btn-default">{{_('Save')}}</div>
<a href="{{ url_for('admin.admin') }}" id="config_back" class="btn btn-default">{{_('Cancel')}}</a>
</div>
</form>
</div>
{% endblock %}
{% block modal %}
{{ filechooser_modal() }}
{{ change_confirm_modal() }}
<div id="InvalidDialog" class="modal fade" role="dialog">
<div class="modal-dialog modal-sm">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header bg-info"></div>
<div class="modal-body text-center">
<p>{{_('New db location is invalid, please enter valid path')}}</p>
<p></p>
<button type="button" class="btn btn-default" id="invalid_confirm" data-dismiss="modal">{{_('OK')}}</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,97 +1,24 @@
{% extends "layout.html" %}
{% block flash %}
<div id="spinning_success" class="row-fluid text-center" style="margin-top: -20px; display:none;">
<div class="alert alert-info"><img id="img-spinner" src="{{ url_for('static', filename='css/libs/images/loading-icon.gif') }}"/></div>
</div>
{% endblock %}
{% block body %}
<div class="discover">
<h2>{{title}}</h2>
<form role="form" method="POST" autocomplete="off">
<form role="form" method="POST" autocomplete="off">
<div class="panel-group col-md-10 col-lg-6">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapseOne">
<span class="glyphicon glyphicon-minus"></span>
{{_('Library Configuration')}}
</a>
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse in">
<div class="panel-body">
<label for="config_calibre_dir">{{_('Location of Calibre Database')}}</label>
<div class="form-group required{% if filepicker %} input-group{% endif %}">
<input type="text" class="form-control" id="config_calibre_dir" name="config_calibre_dir" value="{% if config.config_calibre_dir != None %}{{ config.config_calibre_dir }}{% endif %}" autocomplete="off">
{% if filepicker %}
<span class="input-group-btn">
<button type="button" data-toggle="modal" id="calibre_modal_path" data-link="config_calibre_dir" data-filefilter="metadata.db" data-target="#fileModal" id="library_path" class="btn btn-default"><span class="glyphicon glyphicon-folder-open"></span></button>
</span>
{% endif %}
</div>
{% if not filepicker %}
<div class="form-group">
<label id="filepicker-hint">{{_('To activate serverside filepicker start Calibre-Web with -f option')}}</label>
</div>
{% endif %}
{% if feature_support['gdrive'] %}
<div class="form-group required">
<input type="checkbox" id="config_use_google_drive" name="config_use_google_drive" data-control="gdrive_settings" {% if config.config_use_google_drive %}checked{% endif %} >
<label for="config_use_google_drive">{{_('Use Google Drive?')}}</label>
</div>
<div data-related="gdrive_settings">
{% if gdriveError %}
<div class="form-group">
<label id="gdrive_error">
{{_('Google Drive config problem')}}: {{ gdriveError }}
</label>
</div>
{% else %}
{% if show_authenticate_google_drive and g.user.is_authenticated and config.config_use_google_drive %}
<div class="form-group required">
<a href="{{ url_for('gdrive.authenticate_google_drive') }}" id="gdrive_auth" class="btn btn-primary">{{_('Authenticate Google Drive')}}</a>
</div>
{% else %}
{% if show_authenticate_google_drive and g.user.is_authenticated and not config.config_use_google_drive %}
<div >{{_('Please hit save to continue with setup')}}</div>
{% endif %}
{% if not g.user.is_authenticated and show_login_button %}
<div >{{_('Please finish Google Drive setup after login')}}</div>
{% endif %}
{% if g.user.is_authenticated %}
{% if not show_authenticate_google_drive %}
<div class="form-group required">
<label for="config_google_drive_folder">{{_('Google Drive Calibre folder')}}</label>
<select name="config_google_drive_folder" id="config_google_drive_folder" class="form-control">
{% for gdrivefolder in gdrivefolders %}
<option value="{{ gdrivefolder.title }}" {% if gdrivefolder.title == config.config_google_drive_folder %}selected{% endif %}>{{ gdrivefolder.title }}</option>
{% endfor %}
</select>
</div>
{% if config.config_google_drive_watch_changes_response %}
<label for="config_google_drive_watch_changes_response">{{_('Metadata Watch Channel ID')}}</label>
<div class="form-group input-group required">
<input type="text" class="form-control" name="config_google_drive_watch_changes_response" id="config_google_drive_watch_changes_response" value="{{ config.config_google_drive_watch_changes_response['id'] }} expires on {{ config.config_google_drive_watch_changes_response['expiration'] | strftime }}" autocomplete="off" disabled="">
<span class="input-group-btn"><a href="{{ url_for('gdrive.revoke_watch_gdrive') }}" id="watch_revoke" class="btn btn-primary">{{_('Revoke')}}</a></span>
</div>
{% else %}
<a href="{{ url_for('gdrive.watch_gdrive') }}" id="enable_gdrive_watch" class="btn btn-primary">Enable watch of metadata.db</a>
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
{% if show_back_button %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsetwo">
<a class="accordion-toggle" data-toggle="collapse" href="#collapseone">
<span class="glyphicon glyphicon-plus"></span>
{{_('Server Configuration')}}
</a>
</h4>
</div>
<div id="collapsetwo" class="panel-collapse collapse">
<div id="collapseone" class="panel-collapse collapse">
<div class="panel-body">
<div class="form-group">
<label for="config_port">{{_('Server Port')}}</label>
@ -124,13 +51,13 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsethree">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsetwo">
<span class="glyphicon glyphicon-plus"></span>
{{_('Logfile Configuration')}}
</a>
</h4>
</div>
<div id="collapsethree" class="panel-collapse collapse">
<div id="collapsetwo" class="panel-collapse collapse">
<div class="panel-body">
<div class="form-group">
<label for="config_log_level">{{_('Log Level')}}</label>
@ -159,13 +86,13 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsefive">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsefour">
<span class="glyphicon glyphicon-plus"></span>
{{_('Feature Configuration')}}
</a>
</h4>
</div>
<div id="collapsefive" class="panel-collapse collapse">
<div id="collapsefour" class="panel-collapse collapse">
<div class="panel-body">
<div class="form-group">
<input type="checkbox" id="config_uploading" data-control="upload_settings" name="config_uploading" {% if config.config_uploading %}checked{% endif %}>
@ -379,13 +306,13 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a class="accordion-toggle" data-toggle="collapse" href="#collapseeight">
<a class="accordion-toggle" data-toggle="collapse" href="#collapsefive">
<span class="glyphicon glyphicon-plus"></span>
{{_('External binaries')}}
</a>
</h4>
</div>
<div id="collapseeight" class="panel-collapse collapse">
<div id="collapsefive" class="panel-collapse collapse">
<div class="panel-body">
<label for="config_converterpath">{{_('Path to Calibre E-Book Converter')}}</label>
<div class="form-group input-group">
@ -417,18 +344,10 @@
</div>
</div>
</div>
{% endif %}
</div>
<div class="col-sm-12">
{% if not show_login_button %}
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
{% endif %}
{% if show_back_button %}
<a href="{{ url_for('admin.admin') }}" class="btn btn-default">{{_('Cancel')}}</a>
{% endif %}
{% if show_login_button %}
<a href="{{ url_for('web.login') }}" name="login" class="btn btn-default">{{_('Login')}}</a>
{% endif %}
<button type="button" name="submit" id="config_submit" class="btn btn-default">{{_('Save')}}</button>
<a href="{{ url_for('admin.admin') }}" id="config_back" class="btn btn-default">{{_('Cancel')}}</a>
</div>
</form>
</div>
@ -436,15 +355,3 @@
{% block modal %}
{{ filechooser_modal() }}
{% endblock %}
{% block js %}
<script type="text/javascript">
$(document).on('change', '#config_use_google_drive', function() {
$('#config_google_drive_folder').prop('required', $(this).prop('checked'));
});
$('.collapse').on('shown.bs.collapse', function(){
$(this).parent().find(".glyphicon-plus").removeClass("glyphicon-plus").addClass("glyphicon-minus");
}).on('hidden.bs.collapse', function(){
$(this).parent().find(".glyphicon-minus").removeClass("glyphicon-minus").addClass("glyphicon-plus");
});
</script>
{% endblock %}

View File

@ -6,8 +6,8 @@
{% block body %}
<div class="discover">
<h2>{{title}}</h2>
<form role="form" method="POST" autocomplete="off" class="col-md-10 col-lg-6">
<div class="panel-group">
<form role="form" method="POST" autocomplete="off" >
<div class="panel-group class="col-md-10 col-lg-6">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
@ -71,7 +71,6 @@
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
@ -146,6 +145,7 @@
</div>
</div>
</div>
</div>
<div class="col-sm-12">
<button type="submit" name="submit" class="btn btn-default">{{_('Save')}}</button>
<a href="{{ url_for('admin.admin') }}" class="btn btn-default">{{_('Cancel')}}</a>
@ -157,13 +157,6 @@
{{ restrict_modal() }}
{% endblock %}
{% block js %}
<script type="text/javascript">
$('.collapse').on('shown.bs.collapse', function(){
$(this).parent().find(".glyphicon-plus").removeClass("glyphicon-plus").addClass("glyphicon-minus");
}).on('hidden.bs.collapse', function(){
$(this).parent().find(".glyphicon-minus").removeClass("glyphicon-minus").addClass("glyphicon-plus");
});
</script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-table-editable.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/libs/bootstrap-table/bootstrap-editable.min.js') }}"></script>

View File

@ -111,6 +111,7 @@
</div>
{%endif%}
{% endfor %}
{% block flash %}{% endblock %}
{% if g.current_theme == 1 %}
<div id="loader" hidden="true">
<center>

View File

@ -188,7 +188,7 @@ class User(UserBase, Base):
allowed_column_value = Column(String, default="")
remote_auth_token = relationship('RemoteAuthToken', backref='user', lazy='dynamic')
view_settings = Column(JSON, default={})
kobo_only_shelves_sync = Column(Integer, default=1)
kobo_only_shelves_sync = Column(Integer, default=0)
if oauth_support:

View File

@ -1381,10 +1381,14 @@ def serve_book(book_id, book_format, anyname):
return "File not in Database"
log.info('Serving book: %s', data.name)
if config.config_use_google_drive:
headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
try:
headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
except AttributeError as ex:
log.debug_or_exception(ex)
return "File Not Found"
else:
if book_format.upper() == 'TXT':
try:
@ -1394,11 +1398,11 @@ def serve_book(book_id, book_format, anyname):
return make_response(
rawdata.decode(result['encoding']).encode('utf-8'))
except FileNotFoundError:
log.error("File Not Found")
return "File Not Found"
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format)
@web.route("/download/<int:book_id>/<book_format>", defaults={'anyname': 'None'})
@web.route("/download/<int:book_id>/<book_format>/<anyname>")
@login_required_if_no_ano
@ -1489,9 +1493,9 @@ def register():
@web.route('/login', methods=['GET', 'POST'])
def login():
if not config.db_configured:
log.debug(u"Redirect to initial configuration")
return redirect(url_for('admin.basic_configuration'))
#if not config.db_configured:
# log.debug(u"Redirect to initial configuration")
# return redirect(url_for('admin.basic_configuration'))
if current_user is not None and current_user.is_authenticated:
return redirect(url_for('web.index'))
if config.config_login_type == constants.LOGIN_LDAP and not services.ldap:

View File

@ -3,7 +3,7 @@ Flask-Babel>=0.11.1,<2.1.0
Flask-Login>=0.3.2,<0.5.1
Flask-Principal>=0.3.2,<0.5.1
backports_abc>=0.4
Flask>=1.0.2,<2.0.0
Flask>=1.0.2,<2.1.0
iso-639>=0.4.5,<0.5.0
PyPDF3>=1.0.0,<1.0.4
pytz>=2016.10

File diff suppressed because it is too large Load Diff