1
0
mirror of https://github.com/janeczku/calibre-web synced 2025-09-13 00:05:59 +00:00

Merge branch 'Develop'

This commit is contained in:
Ozzie Isaacs
2025-09-08 12:06:11 +02:00
12 changed files with 1465 additions and 974 deletions

View File

@@ -40,7 +40,7 @@ from flask_babel import gettext as _
from flask_babel import get_locale, format_time, format_datetime, format_timedelta
from sqlalchemy import and_
from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError
from sqlalchemy.exc import IntegrityError, OperationalError, InvalidRequestError, ArgumentError
from sqlalchemy.sql.expression import func, or_, text
from . import constants, logger, helper, services, cli_param
@@ -386,13 +386,12 @@ def list_users():
@user_login_required
@admin_required
def delete_user():
user_ids = request.form.to_dict(flat=False)
users = None
user_ids = request.get_json().get("userid")
message = ""
if "userid[]" in user_ids:
users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids['userid[]'])).all()
elif "userid" in user_ids:
users = ub.session.query(ub.User).filter(ub.User.id == user_ids['userid'][0]).all()
try:
users = ub.session.query(ub.User).filter(ub.User.id.in_(user_ids)).all()
except (ArgumentError):
users = None
count = 0
errors = list()
success = list()
@@ -408,10 +407,10 @@ def delete_user():
errors.append({'type': "danger", 'message': str(ex)})
if count == 1:
log.info("User {} deleted".format(user_ids))
log.info("User {} deleted".format(user_ids[0]))
success = [{'type': "success", 'message': message}]
elif count > 1:
log.info("Users {} deleted".format(user_ids))
log.info("Users {} deleted".format(", ".join([str(user_id) for user_id in user_ids])))
success = [{'type': "success", 'message': _("{} users deleted successfully").format(count)}]
success.extend(errors)
return make_response(jsonify(success))
@@ -618,6 +617,8 @@ def load_dialogtexts(element_id):
texts["main"] = _('Do you really want to delete this domain?')
elif element_id == "btndeluser":
texts["main"] = _('Do you really want to delete this user?')
elif element_id == "btndelbook":
texts["main"] = _('Do you really want to delete this book?')
elif element_id == "delete_shelf":
texts["main"] = _('Are you sure you want to delete this shelf?')
elif element_id == "select_locale":
@@ -626,6 +627,10 @@ def load_dialogtexts(element_id):
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 == "archive_books":
texts["main"] = _('Are you sure you want to change the archive status for the selected book(s)?')
elif element_id == "read_books":
texts["main"] = _('Are you sure you want to change the read status for the selected book(s)?')
elif element_id == "restrictions":
texts["main"] = _('Are you sure you want to change the selected restrictions for the selected user(s)?')
elif element_id == "sidebar_view":

View File

@@ -73,17 +73,18 @@ def edit_required(f):
return inner
@editbook.route("/ajax/delete/<int:book_id>", methods=["POST"])
@editbook.route("/ajax/deletebook", methods=["POST"])
@user_login_required
def delete_book_from_details(book_id):
return delete_book_from_table(book_id, "", True)
def delete_books_ajax():
book_ids = request.get_json().get("bookid")
return check_delete_book(book_ids, "", True)
@editbook.route("/delete/<int:book_id>", defaults={'book_format': ""}, methods=["POST"])
@editbook.route("/delete/<int:book_id>/<string:book_format>", methods=["POST"])
@user_login_required
def delete_book_ajax(book_id, book_format):
return delete_book_from_table(book_id, book_format, False, request.form.to_dict().get('location', ""))
def delete_book(book_id, book_format):
return check_delete_book(book_id, book_format, False, request.form.to_dict().get('location', ""))
@editbook.route("/admin/book/<int:book_id>", methods=['GET'])
@@ -161,7 +162,7 @@ def upload():
return make_response(jsonify(resp))
else:
resp = {"location": url_for('web.show_book', book_id=book_id)}
return make_response(jsonify(resp))
return Response(json.dumps(resp), mimetype='application/json')
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
@@ -213,96 +214,240 @@ def table_get_custom_enum(c_id):
@login_required_if_no_ano
@edit_required
def edit_list_book(param):
vals = request.form.to_dict()
book = calibre_db.get_book(vals['pk'])
calibre_db.create_functions(config)
sort_param = ""
ret = ""
try:
if param == 'series_index':
edit_book_series_index(vals['value'], book)
ret = make_response(jsonify(success=True, newValue=book.series_index))
elif param == 'tags':
edit_book_tags(vals['value'], book)
ret = make_response(jsonify(success=True, newValue=', '.join([tag.name for tag in book.tags])))
elif param == 'series':
edit_book_series(vals['value'], book)
ret = make_response(jsonify(success=True, newValue=', '.join([serie.name for serie in book.series])))
elif param == 'publishers':
edit_book_publisher(vals['value'], book)
ret = make_response(jsonify(success=True,
newValue=', '.join([publisher.name for publisher in book.publishers])))
elif param == 'languages':
invalid = list()
edit_book_languages(vals['value'], book, invalid=invalid)
if invalid:
ret = make_response(jsonify(success=False,
msg='Invalid languages in request: {}'.format(','.join(invalid))))
else:
lang_names = list()
for lang in book.languages:
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
ret = make_response(jsonify(success=True, newValue=', '.join(lang_names)))
elif param == 'author_sort':
book.author_sort = vals['value']
ret = make_response(jsonify(success=True, newValue=book.author_sort))
elif param == 'title':
sort_param = book.sort
if handle_title_on_edit(book, vals.get('value', "")):
rename_error = helper.update_dir_structure(book.id, config.get_book_path())
if not rename_error:
ret = make_response(jsonify(success=True, newValue=book.title))
else:
ret = make_response(jsonify(success=False, msg=rename_error))
elif param == 'sort':
book.sort = vals['value']
ret = make_response(jsonify(success=True,newValue=book.sort))
elif param == 'comments':
edit_book_comments(vals['value'], book)
ret = make_response(jsonify(success=True, newValue=book.comments[0].text))
elif param == 'authors':
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == "true")
rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0])
if not rename_error:
ret = make_response(jsonify(
success=True,
newValue=' & '.join([author.replace('|', ',') for author in input_authors])))
else:
ret = make_response(jsonify(success=False, msg=rename_error))
elif param == 'is_archived':
is_archived = change_archived_books(book.id, vals['value'] == "True",
message="Book {} archive bit set to: {}".format(book.id, vals['value']))
if is_archived:
kobo_sync_status.remove_synced_book(book.id)
return ""
elif param == 'read_status':
ret = helper.edit_book_read_status(book.id, vals['value'] == "True")
if ret:
return ret, 400
elif param.startswith("custom_column_"):
new_val = dict()
new_val[param] = vals['value']
edit_single_cc_data(book.id, book, param[14:], new_val)
# ToDo: Very hacky find better solution
if vals['value'] in ["True", "False"]:
ret = ""
else:
ret = make_response(jsonify(success=True, newValue=vals['value']))
else:
return _("Parameter not found"), 400
book.last_modified = datetime.now(timezone.utc)
vals = request.get_json()
multi = vals.get('multi', False) == "True"
ret_value = edit_book_param(param, vals, multi)
if isinstance(ret_value, dict):
return jsonify(ret_value)
else:
return ret_value
@editbook.route("/ajax/editselectedbooks", methods=['POST'])
@login_required_if_no_ano
@edit_required
def edit_selected_books():
d = request.get_json()
selections = d.get('selections')
title = d.get('title')
title_sort = d.get('title_sort')
author_sort = d.get('author_sort')
authors = d.get('authors')
categories = d.get('categories')
series = d.get('series')
languages = d.get('languages')
publishers = d.get('publishers')
comments = d.get('comments')
if not (
title or title_sort or authors or categories or series or languages or publishers or comments) or not selections:
return _("Parameter not found"), 400
vals = {
"pk": selections,
"value": None,
"checkA": d.get('checkA'),
"checkT": d.get('checkT'),
}
res = list()
if title:
vals['value'] = title
out = edit_book_param('title', vals, True)
if out[0].get('success') != True:
res.extend(out)
if title_sort:
vals['value'] = title_sort
out = edit_book_param('sort', vals, True)
if out[0].get('success') != True:
res.extend(out)
if author_sort:
vals['value'] = author_sort
out = edit_book_param('author_sort', vals, True)
if out[0].get('success') != True:
res.extend(out)
if authors:
vals['value'] = authors
out = edit_book_param('authors', vals, True)
if out[0].get('success') != True:
res.extend(out)
if categories:
vals['value'] = categories
out = edit_book_param('tags', vals, True)
if out[0].get('success') != True:
res.extend(out)
if series:
vals['value'] = series
out = edit_book_param('series', vals, True)
if out[0].get('success') != True:
res.extend(out)
if languages:
vals['value'] = languages
out = edit_book_param('languages', vals, True)
if out[0].get('success') != True:
res.extend(out)
if publishers:
vals['value'] = publishers
out = edit_book_param('publishers', vals, True)
if out[0].get('success') != True:
res.extend(out)
if comments:
vals['value'] = comments
out = edit_book_param('comments', vals, True)
if out[0].get('success') != True:
res.extend(out)
if len(res) == 0:
return jsonify([{'success': True, "msg": _("Changes successfully applied")}])
else:
return jsonify(res)
# Separated from /editbooks so that /editselectedbooks can also use this
#
# param: the property of the book to be changed
# vals - JSON Object:
# {
# 'pk': "the book id",
# 'value': "changes value of param to what's passed here"
# 'checkA': "Optional. Used to check if autosort author is enabled. Assumed as true if not passed"
# 'checkT': "Optional. Used to check if autotitle author is enabled. Assumed as true if not passed"
# }
#
@login_required_if_no_ano
@edit_required
def edit_book_param(param, vals, multi=False):
elements = vals.get('pk',[])
if vals.get('value', None) is None:
return {'success':False, 'msg':_("Value is missing on request")}
if not elements or len(elements) > 1 and multi == False:
return {"success":False, "msg":_("Oops! Selected book is unavailable. File does not exist or is not accessible")}
ret = {}
out = list()
for elem in elements:
book = calibre_db.get_book(elem)
if not book:
ret = {"success": False,
"msg": _("Oops! Selected book is unavailable. File does not exist or is not accessible")}
if multi:
out.append(ret)
continue
else:
return ret
calibre_db.create_functions(config)
sort_param = ""
try:
if param == 'series_index':
edit_book_series_index(vals['value'], book)
ret = {"success":True,
"newValue":book.series_index}
elif param == 'tags':
edit_book_tags(vals['value'], book)
ret = {"success":True,
"newValue":', '.join([tag.name for tag in book.tags])}
elif param == 'series':
edit_book_series(vals['value'], book)
ret = {"success":True,
"newValue":', '.join([serie.name for serie in book.series])}
elif param == 'publishers':
edit_book_publisher(vals['value'], book)
ret = {"success":True,
"newValue":', '.join([publisher.name for publisher in book.publishers])}
elif param == 'languages':
invalid = list()
edit_book_languages(vals['value'], book, invalid=invalid)
if invalid:
ret = {"success": False, "msg": 'Invalid languages in request: {}'.format(','.join(invalid))}
if multi:
out.append(ret)
else:
lang_names = list()
for lang in book.languages:
lang_names.append(isoLanguages.get_language_name(get_locale(), lang.lang_code))
ret = {"success":True,
"newValue":', '.join(lang_names)}
elif param == 'author_sort':
book.author_sort = vals['value']
ret = {"success":True,
"newValue":book.author_sort}
elif param == 'title':
sort_param = book.sort
if handle_title_on_edit(book, vals.get('value', "")):
rename_error = helper.update_dir_structure(book.id, config.get_book_path())
if not rename_error:
calibre_db.session.commit()
ret = {"success":True,
"newValue":book.title}
else:
calibre_db.session.rollback()
ret = {"success":False, "msg":rename_error}
if multi:
out.append(ret)
elif param == 'sort':
book.sort = vals['value']
ret = {"success":True,
"newValue":book.sort}
elif param == 'comments':
edit_book_comments(vals['value'], book)
ret = {"success":True,
"newValue":book.comments[0].text}
elif param == 'authors':
input_authors, __ = handle_author_on_edit(book, vals['value'], vals.get('checkA', None) == True)
rename_error = helper.update_dir_structure(book.id, config.get_book_path(), input_authors[0])
if not rename_error:
calibre_db.session.commit()
ret = {"success":True,
"newValue":' & '.join([author.replace('|', ',') for author in input_authors])}
else:
calibre_db.session.rollback()
ret = {"success":False, "msg":rename_error}
if multi:
out.append(ret)
elif param == 'is_archived':
is_archived = change_archived_books(book.id, vals['value'] == "True",
message="Book {} archive bit set to: {}".format(book.id,
vals['value']))
if is_archived:
kobo_sync_status.remove_synced_book(book.id)
continue
elif param == 'read_status':
error = helper.edit_book_read_status(book.id, vals['value'] == "True")
if error:
if multi:
out.append({"success":False, "msg":error})
continue
else:
return error, 400
continue
elif param.startswith("custom_column_"):
new_val = dict()
new_val[param] = vals['value']
edit_single_cc_data(book.id, book, param[14:], new_val)
# ToDo: Very hacky find better solution
if vals['value'] in ["True", "False"]:
ret = {}
else:
ret = {"success":True, "newValue":vals['value']}
else:
if multi:
out.append({"success":False, "msg":_("Parameter not found")})
continue
return _("Parameter not found"), 400
book.last_modified = datetime.now(timezone.utc)
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_param
calibre_db.session.commit()
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
ret = make_response(jsonify(success=False,
msg='Database error: {}'.format(e.orig if hasattr(e, "orig") else e)))
return ret
# revert change for sort if automatic fields link is deactivated
if param == 'title' and vals.get('checkT') == False:
book.sort = sort_param
calibre_db.session.commit()
except (OperationalError, IntegrityError, StaleDataError, AttributeError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
ret = {"success":False, "msg":'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}
if multi:
out.append(ret)
if multi:
if len(out) > 0:
return out
else:
return [ret]
else:
return ret
@editbook.route("/ajax/sort_value/<field>/<int:bookid>")
@@ -337,6 +482,55 @@ def simulate_merge_list_book():
return make_response(jsonify({'to': to_book, 'from': from_book}))
return ""
@editbook.route("/ajax/displayselectedbooks", methods=['POST'])
@user_login_required
@edit_required
def display_selected_books():
vals = request.get_json().get('selections')
books = []
if vals:
for book_id in vals:
books.append(calibre_db.get_book(book_id).title)
return json.dumps({'books': books})
return ""
@editbook.route("/ajax/archiveselectedbooks", methods=['POST'])
@login_required_if_no_ano
@edit_required
def archive_selected_books():
vals = request.get_json().get('selections')
state = request.get_json().get('archive')
if vals:
for book_id in vals:
is_archived = change_archived_books(book_id, state,
message="Book {} archive bit set to: {}".format(book_id, state))
if is_archived:
kobo_sync_status.remove_synced_book(book_id)
return json.dumps({'success': True})
return ""
@editbook.route("/ajax/readselectedbooks", methods=['POST'])
@user_login_required
@edit_required
def read_selected_books():
vals = request.get_json().get('selections')
markAsRead = request.get_json().get('markAsRead')
if vals:
try:
for book_id in vals:
ret = helper.edit_book_read_status(book_id, markAsRead)
except (OperationalError, IntegrityError, StaleDataError) as e:
calibre_db.session.rollback()
log.error_or_exception("Database error: {}".format(e))
ret = Response(json.dumps({'success': False,
'msg': 'Database error: {}'.format(e.orig if hasattr(e, "orig") else e)}),
mimetype='application/json')
return json.dumps({'success': True})
return ""
@editbook.route("/ajax/mergebooks", methods=['POST'])
@user_login_required
@@ -371,7 +565,7 @@ def merge_list_book():
element.format,
element.uncompressed_size,
to_name))
delete_book_from_table(from_book.id, "", True)
check_delete_book([from_book.id], "", True)
return make_response(jsonify(success=True))
return ""
@@ -648,8 +842,9 @@ def prepare_authors(authr, calibre_path, gdrive=False):
all_new_name = helper.get_valid_filename(one_book.title, chars=42) + ' - ' \
+ helper.get_valid_filename(renamed_author.name, chars=42)
# change location in database to new author/title path
helper.rename_all_files_on_change(one_book, new_path, new_path, all_new_name, gdrive)
error = helper.rename_all_files_on_change(one_book, new_path, new_path, all_new_name, gdrive)
if error:
flash(error)
return input_authors
@@ -841,86 +1036,109 @@ def delete_whole_book(book_id, book):
calibre_db.session.query(db.Books).filter(db.Books.id == book_id).delete()
def render_delete_book_result(book_format, json_response, warning, book_id, location=""):
def render_delete_book_result(book_format, book_id, location=""):
if book_format:
if json_response:
return jsonify([warning, {"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "success",
"format": book_format,
"message": _('Book Format Successfully Deleted')}])
else:
flash(_('Book Format Successfully Deleted'), category="success")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
flash(_('Book Format Successfully Deleted'), category="success")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
else:
if json_response:
return jsonify([warning, {"location": get_redirect_location(location, "web.index"),
"type": "success",
"format": book_format,
"message": _('Book Successfully Deleted')}])
else:
flash(_('Book Successfully Deleted'), category="success")
return redirect(get_redirect_location(location, "web.index"))
flash(_('Book Successfully Deleted'), category="success")
return redirect(get_redirect_location(location, "web.index"))
def delete_book_from_table(book_id, book_format, json_response, location=""):
warning = {}
def check_delete_book(book_id, book_format, json_response, location=""):
if current_user.role_delete_books():
book = calibre_db.get_book(book_id)
if book:
try:
result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper())
if not result:
if json_response:
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": error}])
else:
flash(error, category="error")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
if error:
if json_response:
warning = {"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "warning",
"format": "",
"message": error}
else:
flash(error, category="warning")
if not book_format:
delete_whole_book(book_id, book)
else:
calibre_db.session.query(db.Data).filter(db.Data.book == book.id).\
filter(db.Data.format == book_format).delete()
if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']:
kobo_sync_status.remove_synced_book(book.id, True)
calibre_db.session.commit()
except Exception as ex:
log.error_or_exception(ex)
calibre_db.session.rollback()
if json_response:
return jsonify([{"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": ex}])
else:
flash(str(ex), category="error")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
if json_response:
# if json response is set, it's possible to delete more than one book, but never a format is deleted
res = list()
for b in book_id:
ret = delete_book_from_table(b)
if ret:
res.extend([ret])
if len(res) == 0:
return [{"location": get_redirect_location(location, "web.index"),
"type": "success",
"format": "",
"message": _('Book Successfully Deleted')}]
return jsonify(res)
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
return render_delete_book_result(book_format, json_response, warning, book_id, location)
return delete_book_from_UI(book_id, book_format, location)
message = _("You are missing permissions to delete books")
if json_response:
return jsonify({"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": message})
try:
return jsonify({"location": url_for("edit-book.show_edit_book", book_id=int(book_id)),
"type": "danger",
"format": "",
"message": message})
except TypeError as e:
return jsonify({"location": url_for("web.index"), "type": "danger", "format": "",
"message": str(e)})
else:
flash(message, category="error")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
def delete_book_from_UI(book_id, book_format, location=""):
book = calibre_db.get_book(book_id)
if book:
try:
result, error = helper.delete_book(book, config.get_book_path(), book_format=book_format.upper())
if not result:
flash(error, category="error")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
if error:
flash(error, category="warning")
if not book_format:
delete_whole_book(book_id, book)
else:
calibre_db.session.query(db.Data).filter(db.Data.book == book.id). \
filter(db.Data.format == book_format).delete()
if book_format.upper() in ['KEPUB', 'EPUB', 'EPUB3']:
kobo_sync_status.remove_synced_book(book.id, True)
calibre_db.session.commit()
except Exception as ex:
log.error_or_exception(ex)
calibre_db.session.rollback()
flash(str(ex), category="error")
return redirect(url_for('edit-book.show_edit_book', book_id=book_id))
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
return render_delete_book_result(book_format, book_id, location)
def delete_book_from_table(book_id):
book = calibre_db.get_book(book_id)
if book:
try:
result, error = helper.delete_book(book, config.get_book_path(), book_format="")
if not result:
return {"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": error}
delete_whole_book(book_id, book)
calibre_db.session.commit()
if error:
return {"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "warning",
"format": "",
"message": error}
except Exception as ex:
log.error_or_exception(ex)
calibre_db.session.rollback()
return {"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": ex}
else:
# book not found
log.error('Book with id "%s" could not be deleted: not found', book_id)
return {"location": url_for("edit-book.show_edit_book", book_id=book_id),
"type": "danger",
"format": "",
"message": _('Book with id "{}" could not be deleted: not found'.format(book_id))}
def render_edit_book(book_id):
cc = calibre_db.session.query(db.CustomColumns).filter(db.CustomColumns.datatype.notin_(db.cc_exceptions)).all()
book = calibre_db.get_filtered_book(book_id, allow_show_archived=True)

View File

@@ -307,18 +307,16 @@ def edit_book_read_status(book_id, read_status=None):
if not config.config_read_column:
book = ub.session.query(ub.ReadBook).filter(and_(ub.ReadBook.user_id == int(current_user.id),
ub.ReadBook.book_id == book_id)).first()
if book:
if read_status is None:
if book.read_status == ub.ReadBook.STATUS_FINISHED:
book.read_status = ub.ReadBook.STATUS_UNREAD
else:
book.read_status = ub.ReadBook.STATUS_FINISHED
else:
book.read_status = ub.ReadBook.STATUS_FINISHED if read_status else ub.ReadBook.STATUS_UNREAD
else:
if not book:
read_book = ub.ReadBook(user_id=current_user.id, book_id=book_id)
read_book.read_status = ub.ReadBook.STATUS_FINISHED
book = read_book
if read_status is None:
if book.read_status == ub.ReadBook.STATUS_FINISHED:
book.read_status = ub.ReadBook.STATUS_UNREAD
else:
book.read_status = ub.ReadBook.STATUS_FINISHED
else:
book.read_status = ub.ReadBook.STATUS_FINISHED if read_status == True else ub.ReadBook.STATUS_UNREAD
if not book.kobo_reading_state:
kobo_reading_state = ub.KoboReadingState(user_id=current_user.id, book_id=book_id)
kobo_reading_state.current_bookmark = ub.KoboBookmark()
@@ -396,10 +394,16 @@ def delete_book_file(book, calibrepath, book_format=None):
def rename_all_files_on_change(one_book, new_path, old_path, all_new_name, gdrive=False):
for file_format in one_book.data:
if not gdrive:
if not os.path.exists(new_path):
os.makedirs(new_path)
shutil.move(os.path.join(old_path, file_format.name + '.' + file_format.format.lower()),
os.path.join(new_path, all_new_name + '.' + file_format.format.lower()))
try:
if not os.path.exists(new_path):
os.makedirs(new_path)
shutil.move(os.path.join(old_path, file_format.name + '.' + file_format.format.lower()),
os.path.join(new_path, all_new_name + '.' + file_format.format.lower()))
except (PermissionError, FileNotFoundError) as ex:
log.error("Moving book-id %s folder %s failed: %s", one_book.id, new_path, ex)
return _("Moving book path of Book %(book_id)s to: '%(src)s' failed with error: %(error)s",
book_id=one_book.id, src=new_path, error=str(ex))
else:
g_file = gd.getFileFromEbooksFolder(old_path,
file_format.name + '.' + file_format.format.lower())
@@ -412,6 +416,7 @@ def rename_all_files_on_change(one_book, new_path, old_path, all_new_name, gdriv
# change name in Database
file_format.name = all_new_name
return False
def rename_author_path(first_author, old_author_dir, renamed_author, calibre_path="", gdrive=False):
@@ -466,14 +471,15 @@ def update_dir_structure_file(book_id, calibre_path, original_filepath, new_auth
db_filename,
original_filepath,
path)
new_path = os.path.join(calibre_path, new_author_dir, new_title_dir).replace('\\', '/')
all_new_name = get_valid_filename(local_book.title, chars=42) + ' - ' \
+ get_valid_filename(new_author, chars=42)
# Book folder already moved, only files need to be renamed
rename_all_files_on_change(local_book, new_path, new_path, all_new_name)
if not error:
new_path = os.path.join(calibre_path, new_author_dir, new_title_dir).replace('\\', '/')
all_new_name = get_valid_filename(local_book.title, chars=42) + ' - ' \
+ get_valid_filename(new_author, chars=42)
# Book folder already moved, only files need to be renamed
renameerror = rename_all_files_on_change(local_book, new_path, new_path, all_new_name)
if error:
return error
if error or renameerror:
return error or renameerror
return False
@@ -517,7 +523,7 @@ def update_dir_structure_gdrive(book_id, first_author):
if titledir != new_titledir or authordir != new_authordir :
all_new_name = get_valid_filename(book.title, chars=42) + ' - ' \
+ get_valid_filename(new_authordir, chars=42)
rename_all_files_on_change(book, book.path, book.path, all_new_name, gdrive=True) # todo: Move filenames on gdrive
return rename_all_files_on_change(book, book.path, book.path, all_new_name, gdrive=True) # todo: Move filenames on gdrive
return False
@@ -553,33 +559,13 @@ def move_files_on_change(calibre_path, new_author_dir, new_titledir, localbook,
log.error("Deleting authorpath for book %s failed: %s", localbook.id, ex)
# change location in database to new author/title path
localbook.path = os.path.join(new_author_dir, new_titledir).replace('\\', '/')
except OSError as ex:
except (OSError, FileNotFoundError) as ex:
log.error_or_exception("Rename title from {} to {} failed with error: {}".format(path, new_path, ex))
return _("Rename title from: '%(src)s' to '%(dest)s' failed with error: %(error)s",
src=path, dest=new_path, error=str(ex))
return False
def rename_files_on_change(first_author,
renamed_author,
local_book,
original_filepath="",
path="",
calibre_path="",
gdrive=False):
# Rename all files from old names to new names
#try:
#clean_author_database(renamed_author, calibre_path, gdrive=gdrive)
#if first_author and first_author not in renamed_author:
# clean_author_database([first_author], calibre_path, local_book, gdrive)
#if not gdrive and not renamed_author and not original_filepath and len(os.listdir(os.path.dirname(path))) == 0:
# shutil.rmtree(os.path.dirname(path))
#except (OSError, FileNotFoundError) as ex:
# log.error_or_exception("Error in rename file in path {}".format(ex))
# return _("Error in rename file in path: {}".format(str(ex)))
return False
def delete_book_gdrive(book, book_format):
error = None
if book_format:

View File

@@ -51,13 +51,15 @@ def remove_synced_book(book_id, all=False, session=None):
ub.session_commit(_session=session)
# If state == none, it will toggle the archive state of the passed book_id.
# state = true archives it, state = false unarchives it
def change_archived_books(book_id, state=None, message=None):
archived_book = ub.session.query(ub.ArchivedBook).filter(and_(ub.ArchivedBook.user_id == int(current_user.id),
ub.ArchivedBook.book_id == book_id)).first()
if not archived_book:
if not archived_book: # and (state == True or state == None):
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
archived_book.is_archived = state if state else not archived_book.is_archived
archived_book.is_archived = state if state != None else not archived_book.is_archived
archived_book.last_modified = datetime.now(timezone.utc) # toDo. Check utc timestamp
ub.session.merge(archived_book)

View File

@@ -26,3 +26,14 @@ body.serieslist.grid-view div.container-fluid > div > div.col-sm-10::before {
input.datepicker {color: transparent}
input.datepicker:focus {color: transparent}
input.datepicker:focus + input {color: #555}
.col-sm-3.col-lg-2.col-xs-6.book.session {
margin-left: 0;
margin-right: 0;
}
@media only screen and (max-width: 767px) {
.row-fluid > .col-sm-2 {
visibility: hidden;
}
}

View File

@@ -229,10 +229,12 @@ $("#delete_confirm").click(function(event) {
postButton(event, getPath() + "/delete/" + deleteId + "/" + bookFormat);
} else {
if (ajaxResponse) {
path = getPath() + "/ajax/delete/" + deleteId;
$.ajax({
method:"post",
url: path,
url: getPath() + "/ajax/deletebook",
method: "post",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({"bookid": [deleteId]}),
timeout: 900,
success:function(data) {
data.forEach(function(item) {

View File

@@ -81,20 +81,78 @@ $(function() {
$("#merge_books").addClass("disabled");
$("#merge_books").attr("aria-disabled", true);
}
if (selections.length >= 1) {
$("#delete_selected_books").removeClass("disabled");
$("#delete_selected_books").attr("aria-disabled", false);
$("#archive_selected_books").removeClass("disabled");
$("#archive_selected_books").attr("aria-disabled", false);
$("#unarchive_selected_books").removeClass("disabled");
$("#unarchive_selected_books").attr("aria-disabled", false);
$("#read_selected_books").removeClass("disabled");
$("#read_selected_books").attr("aria-disabled", false);
$("#unread_selected_books").removeClass("disabled");
$("#unread_selected_books").attr("aria-disabled", false);
$("#edit_selected_books").removeClass("disabled");
$("#edit_selected_books").attr("aria-disabled", false);
} else {
$("#delete_selected_books").addClass("disabled");
$("#delete_selected_books").attr("aria-disabled", true);
$("#archive_selected_books").addClass("disabled");
$("#archive_selected_books").attr("aria-disabled", true);
$("#unarchive_selected_books").addClass("disabled");
$("#unarchive_selected_books").attr("aria-disabled", true);
$("#read_selected_books").addClass("disabled");
$("#read_selected_books").attr("aria-disabled", true);
$("#unread_selected_books").addClass("disabled");
$("#unread_selected_books").attr("aria-disabled", true);
$("#edit_selected_books").addClass("disabled");
$("#edit_selected_books").attr("aria-disabled", true);
}
if (selections.length < 1) {
$("#delete_selection").addClass("disabled");
$("#delete_selection").attr("aria-disabled", true);
// $("#book_delete_selection").addClass("disabled");
// $("#book_delete_selection").attr("aria-disabled", true);
$("#table_xchange").addClass("disabled");
$("#table_xchange").attr("aria-disabled", true);
} else {
$("#delete_selection").removeClass("disabled");
$("#delete_selection").attr("aria-disabled", false);
// $("#book_delete_selection").removeClass("disabled");
// $("#book_delete_selection").attr("aria-disabled", false);
$("#table_xchange").removeClass("disabled");
$("#table_xchange").attr("aria-disabled", false);
}
handle_header_buttons();
});
$("#delete_selection").click(function() {
// Small block to initialize the state of the author/title sort inputs in metadata form
{
let checkA = $('#autoupdate_authorsort').prop('checked');
$('#author_sort_input').prop('disabled', checkA);
let checkT = $('#autoupdate_titlesort').prop('checked');
$('#title_sort_input').prop('disabled', checkT);
}
// Disable/enable author and title sort input in respect to auto-update title/author sort being checked on or not
$("#autoupdate_authorsort").on('change', function(event) {
let checkA = $('#autoupdate_authorsort').prop('checked');
$('#author_sort_input').prop('disabled', checkA);
})
$("#autoupdate_titlesort").on('change', function(event) {
let checkT = $('#autoupdate_titlesort').prop('checked');
$('#title_sort_input').prop('disabled', checkT);
})
/////
$("#book_delete_selection").click(function () {
$("#books-table").bootstrapTable("uncheckAll");
});
@@ -135,6 +193,250 @@ $(function() {
});
});
$("#edit_selected_books").click(function(event) {
if ($(this).hasClass("disabled")) {
event.stopPropagation()
} else {
$('#edit_selected_modal').modal("show");
}
});
$("#edit_selected_confirm").click(function(event) {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/editselectedbooks",
data: JSON.stringify({
"selections": selections,
"title": $("#title_input").val(),
"title_sort": $("#title_sort_input").val(),
"author_sort": $("#author_sort_input").val(),
"authors": $("#authors_input").val(),
"categories": $("#categories_input").val(),
"series": $("#series_input").val(),
"languages": $("#languages_input").val(),
"publishers": $("#publishers_input").val(),
"comments": $("#comments_input").val().toString(),
"checkA": $('#autoupdate_authorsort').prop('checked'),
"checkT": $('#autoupdate_titlesort').prop('checked')
}),
success: function success(data) {
let result = "";
$("#books-table").bootstrapTable("refresh");
$("#books-table").bootstrapTable("uncheckAll");
$("#title_input").val("");
$("#title_sort_input").val("");
$("#author_sort_input").val("");
$("#authors_input").val("");
$("#categories_input").val("");
$("#series_input").val("");
$("#languages_input").val("");
$("#publishers_input").val("");
$("#comments_input").val("");
$("#flash_success").remove();
$("#flash_danger").remove();
if (!jQuery.isEmptyObject(data)) {
data.forEach(function(item) {
if (item.success === true) {
result = "success";
} else {
result = "danger";
}
$(".navbar").after('<div class="row-fluid text-center">' +
'<div id="flash_' + result + '" class="alert alert-' + result + '">' + item.msg + '</div>' +
'</div>');
});
}
$(".table.table-striped").bootstrapTable("refresh");
// handleListServerResponse(data);
}
});
});
$(document).on('click', '#archive_selected_books', function(event) {
if ($(this).hasClass("disabled")) {
event.stopPropagation()
} else {
$('#archive_selected_modal').modal("show");
}
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/displayselectedbooks",
data: JSON.stringify({"selections":selections}),
success: function success(booTitles) {
$('#display-archive-selected-books').empty();
$.each(booTitles.books, function(i, item) {
$("<span>- " + item + "</span><p></p>").appendTo("#display-archive-selected-books");
});
}
});
});
/*$(document).on('click', '#archive_selected_confirm', function(event) {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/archiveselectedbooks",
data: JSON.stringify({"selections":selections, "archive": true}),
success: function success(booTitles) {
$("#books-table").bootstrapTable("refresh");
$("#books-table").bootstrapTable("uncheckAll");
}
});
});
$(document).on('click', '#unarchive_selected_books', function(event) {
if ($(this).hasClass("disabled")) {
event.stopPropagation()
} else {
$('#unarchive_selected_modal').modal("show");
}
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/displayselectedbooks",
data: JSON.stringify({"selections":selections}),
success: function success(booTitles) {
$('#display-unarchive-selected-books').empty();
$.each(booTitles.books, function(i, item) {
$("<span>- " + item + "</span><p></p>").appendTo("#display-unarchive-selected-books");
});
}
});
});
$(document).on('click', '#unarchive_selected_confirm', function(event) {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/archiveselectedbooks",
data: JSON.stringify({"selections":selections, "archive": false}),
success: function success(booTitles) {
$("#books-table").bootstrapTable("refresh");
$("#books-table").bootstrapTable("uncheckAll");
}
});
});
$(document).on('click', '#delete_selected_books', function(event) {
if ($(this).hasClass("disabled")) {
event.stopPropagation()
} else {
$('#delete_selected_modal').modal("show");
}
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/displayselectedbooks",
data: JSON.stringify({"selections":selections}),
success: function success(booTitles) {
$('#display-delete-selected-books').empty();
$.each(booTitles.books, function(i, item) {
$("<span>- " + item + "</span><p></p>").appendTo("#display-delete-selected-books");
});
}
});
});
$(document).on('click', '#delete_selected_confirm', function(event) {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/deleteselectedbooks",
data: JSON.stringify({"selections":selections}),
success: function success(booTitles) {
$("#books-table").bootstrapTable("refresh");
$("#books-table").bootstrapTable("uncheckAll");
}
});
});
$(document).on('click', '#read_selected_books', function(event) {
if ($(this).hasClass("disabled")) {
event.stopPropagation()
} else {
$('#read_selected_modal').modal("show");
}
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/displayselectedbooks",
data: JSON.stringify({"selections":selections}),
success: function success(booTitles) {
$('#display-read-selected-books').empty();
$.each(booTitles.books, function(i, item) {
$("<span>- " + item + "</span><p></p>").appendTo("#display-read-selected-books");
});
}
});
});
$(document).on('click', '#read_selected_confirm', function(event) {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/readselectedbooks",
data: JSON.stringify({"selections":selections, "markAsRead": true}),
success: function success(booTitles) {
$("#books-table").bootstrapTable("refresh");
$("#books-table").bootstrapTable("uncheckAll");
}
});
});
$(document).on('click', '#unread_selected_books', function(event) {
if ($(this).hasClass("disabled")) {
event.stopPropagation()
} else {
$('#unread_selected_modal').modal("show");
}
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/displayselectedbooks",
data: JSON.stringify({"selections":selections}),
success: function success(booTitles) {
$('#display-unread-selected-books').empty();
$.each(booTitles.books, function(i, item) {
$("<span>- " + item + "</span><p></p>").appendTo("#display-unread-selected-books");
});
}
});
});
$(document).on('click', '#unread_selected_confirm', function(event) {
$.ajax({
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: getPath() + "/ajax/readselectedbooks",
data: JSON.stringify({"selections":selections, "markAsRead": false}),
success: function success(booTitles) {
$("#books-table").bootstrapTable("refresh");
$("#books-table").bootstrapTable("uncheckAll");
}
});
});*/
$("#table_xchange").click(function() {
$.ajax({
method:"post",
@@ -157,6 +459,10 @@ $(function() {
editable: {
mode: "inline",
emptytext: "<span class='glyphicon glyphicon-plus'></span>",
ajaxOptions: {
contentType: "application/json; charset=utf-8",
dataType: "json",
},
success: function (response, __) {
if (!response.success) return response.msg;
return {newValue: response.newValue};
@@ -164,7 +470,8 @@ $(function() {
params: function (params) {
params.checkA = $('#autoupdate_authorsort').prop('checked');
params.checkT = $('#autoupdate_titlesort').prop('checked');
return params
params.pk = [params.pk];
return JSON.stringify(params);
}
}
};
@@ -198,7 +505,7 @@ $(function() {
searchAlign: "left",
showSearchButton : true,
searchOnEnterKey: true,
checkboxHeader: false,
checkboxHeader: true,
maintainMetaData: true,
responseHandler: responseHandler,
columns: column,
@@ -212,7 +519,7 @@ $(function() {
$.ajax({
method:"get",
dataType: "json",
url: window.location.pathname + "/../ajax/sort_value/" + field + "/" + row.id,
url: getPath() + "/ajax/sort_value/" + field + "/" + row.id,
success: function success(data) {
var key = Object.keys(data)[0];
$("#books-table").bootstrapTable("updateCellByUniqueId", {
@@ -224,6 +531,66 @@ $(function() {
});
}
},
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);
});
},
onPostHeader() {
$(".form-check").each(function () {
var item = $(this).parent();
var parent = item.parent().parent();
if (parent.prop('nodeName') === "TH") {
item.prependTo(parent);
}
});
if ($(".button_head").length) {
if (!$._data($(".button_head").get(0), "events")) {
$(".button_head").on("click", function () {
var result = $('#books-table').bootstrapTable('getSelections').map(a => a.id);
confirmDialog(
"btndelbook",
"GeneralDeleteModal",
0,
function () {
$.ajax({
method: "post",
url: getPath() + "/ajax/deletebook",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({"bookid": result}),
success: function (data) {
selections = selections.filter((el) => !result.includes(el));
handleListServerResponse(data);
},
error: function (data) {
handleListServerResponse([{type: "danger", message: data.responseText}])
},
});
}
);
});
}
}
if ($(".check_head").length) {
if (!$._data($(".check_head").get(0), "events")) {
$(".check_head").on("change", function () {
var val = $(this).data("set");
var name = $(this).data("name");
var data = $(this).data("val");
bookCheckboxHeader(val, name, data);
});
}
}
},
// eslint-disable-next-line no-unused-vars
onColumnSwitch: function (field, checked) {
var visible = $("#books-table").bootstrapTable("getVisibleColumns");
@@ -240,10 +607,16 @@ $(function() {
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../ajax/table_settings",
url: getPath() + "/ajax/table_settings",
data: "{" + st + "}",
});
handle_header_buttons();
},
onLoadSuccess: function() {
$("input:radio.check_head:checked").each(function () {
$(this).prop('checked', false);
});
}
});
$("#domain_allow_submit").click(function(event) {
@@ -252,7 +625,7 @@ $(function() {
$(this).closest("form").submit();
$.ajax ({
method:"get",
url: window.location.pathname + "/../../ajax/domainlist/1",
url: getPath() + "/ajax/domainlist/1",
async: true,
timeout: 900,
success:function(data) {
@@ -273,7 +646,7 @@ $(function() {
$(this).closest("form").submit();
$.ajax ({
method:"get",
url: window.location.pathname + "/../../ajax/domainlist/0",
url: getPath() + "/ajax/domainlist/0",
async: true,
timeout: 900,
success:function(data) {
@@ -291,12 +664,12 @@ $(function() {
function domainHandle(domainId) {
$.ajax({
method:"post",
url: window.location.pathname + "/../../ajax/deletedomain",
url: getPath() + "/ajax/deletedomain",
data: {"domainid":domainId}
});
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/domainlist/1",
url: getPath() + "/ajax/domainlist/1",
async: true,
timeout: 900,
success:function(data) {
@@ -305,7 +678,7 @@ $(function() {
});
$.ajax({
method:"get",
url: window.location.pathname + "/../../ajax/domainlist/0",
url: getPath() + "/ajax/domainlist/0",
async: true,
timeout: 900,
success:function(data) {
@@ -541,7 +914,7 @@ $(function() {
method:"post",
contentType: "application/json; charset=utf-8",
dataType: "json",
url: window.location.pathname + "/../../ajax/user_table_settings",
url: getPath() + "/ajax/user_table_settings",
data: "{" + st + "}",
});
handle_header_buttons();
@@ -567,8 +940,8 @@ $(function() {
function handle_header_buttons () {
if (selections.length < 1) {
$("#user_delete_selection").addClass("disabled");
$("#user_delete_selection").attr("aria-disabled", true);
$(".mass_selection").addClass("disabled");
$(".mass_selection").attr("aria-disabled", true);
$(".check_head").attr("aria-disabled", true);
$(".check_head").attr("disabled", true);
$(".check_head").prop('checked', false);
@@ -580,8 +953,8 @@ function handle_header_buttons () {
$(".multi_selector").attr("disabled", true);
$(".header_select").attr("disabled", true);
} else {
$("#user_delete_selection").removeClass("disabled");
$("#user_delete_selection").attr("aria-disabled", false);
$(".mass_selection").removeClass("disabled");
$(".mass_selection").attr("aria-disabled", false);
$(".check_head").attr("aria-disabled", false);
$(".check_head").removeAttr("disabled");
$(".button_head").attr("aria-disabled", false);
@@ -590,8 +963,10 @@ function handle_header_buttons () {
$(".multi_head").removeClass("hidden");
$(".multi_selector").attr("aria-disabled", false);
$(".multi_selector").removeAttr("disabled");
$('.multi_selector').selectpicker('refresh');
$(".header_select").removeAttr("disabled");
if (typeof $.fn.selectpicker === "function") {
$('.multi_selector').selectpicker('refresh');
}
}
}
@@ -718,7 +1093,7 @@ function loadSuccess() {
$("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);
$("input[data-name='sidebar_read_and_unread'][data-pk='"+guest.data("pk")+"']").prop("disabled", true);
$(".user-remove[data-pk='"+guest.data("pk")+"']").hide();
$(".user-remove[data-pk='" + guest.data("pk") + "']").hide();
}
function move_header_elements() {
@@ -760,7 +1135,7 @@ function move_header_elements() {
function () {
$.ajax({
method: "post",
url: window.location.pathname + "/../../ajax/editlistusers/" + field,
url: getPath() + "/ajax/editlistusers/" + field,
data: {"pk": result, "value": values, "action": val},
success: function (data) {
handleListServerResponse(data);
@@ -774,7 +1149,6 @@ function move_header_elements() {
});
}
}
$("#user_delete_selection").click(function () {
$("#user-table").bootstrapTable("uncheckAll");
});
@@ -805,8 +1179,10 @@ function move_header_elements() {
function () {
$.ajax({
method: "post",
url: window.location.pathname + "/../../ajax/deleteuser",
data: {"userid": result},
url: getPath() + "/ajax/deleteuser",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({"userid": result}),
success: function (data) {
selections = selections.filter((el) => !result.includes(el));
handleListServerResponse(data);
@@ -832,7 +1208,7 @@ function handleListServerResponse (data) {
'</div>');
});
}
$("#user-table").bootstrapTable("refresh");
$(".table.table-striped").bootstrapTable("refresh");
}
function checkboxChange(checkbox, userId, field, field_index) {
@@ -847,30 +1223,30 @@ function checkboxChange(checkbox, userId, field, field_index) {
});
}
function BookCheckboxChange(checkbox, userId, field) {
function BookCheckboxChange(checkbox, bookId, field) {
var value = checkbox.checked ? "True" : "False";
var element = checkbox;
$.ajax({
method: "post",
url: getPath() + "/ajax/editbooks/" + field,
data: {"pk": userId, "value": value},
data: JSON.stringify({"pk": [bookId], "value": value}),
contentType: "application/json; charset=utf-8",
dataType: "json",
error: function(data) {
element.checked = !element.checked;
handleListServerResponse([{type:"danger", message:data.responseText}])
},
success: handleListServerResponse
});
console.log("test");
}
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,
url: getPath() + "/ajax/editlistusers/" + field,
data: {"pk": result, "value": element.value},
error: function (data) {
handleListServerResponse([{type:"danger", message:data.responseText}])
@@ -883,12 +1259,35 @@ function selectHeader(element, field) {
}
}
function bookCheckboxHeader(CheckboxState, text, field_index) {
confirmDialog(text, "GeneralChangeModal", 0, function() {
var result = $('#books-table').bootstrapTable('getSelections').map(a => a.id);
$.ajax({
method: "post",
url: getPath() + "/ajax/editbooks/" + field_index,
data: JSON.stringify({"pk": result, "field_index": field_index, "value": CheckboxState, multi: "True"}),
contentType: "application/json; charset=utf-8",
dataType: "json",
error: function (data) {
handleListServerResponse([{type:"danger", message:data.responseText}])
},
success: function (data) {
handleListServerResponse (data, true)
},
});
},function() {
$("input:radio.check_head:checked").each(function() {
$(this).prop('checked', false);
});
});
}
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,
url: getPath() + "/ajax/editlistusers/" + field,
data: {"pk": result, "field_index": field_index, "value": CheckboxState},
error: function (data) {
handleListServerResponse([{type:"danger", message:data.responseText}])
@@ -904,7 +1303,7 @@ function checkboxHeader(CheckboxState, field, field_index) {
});
}
function deleteUser(a,id){
function deleteUser(a, id){
confirmDialog(
"btndeluser",
"GeneralDeleteModal",
@@ -912,8 +1311,10 @@ function deleteUser(a,id){
function() {
$.ajax({
method:"post",
url: window.location.pathname + "/../../ajax/deleteuser",
data: {"userid":id},
url: getPath() + "/ajax/deleteuser",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({"userid": [id]}),
success: function (data) {
userId = parseInt(id, 10);
selections = selections.filter(item => item !== userId);
@@ -940,8 +1341,10 @@ function storeLocation() {
function user_handle (userId) {
$.ajax({
method:"post",
url: window.location.pathname + "/../../ajax/deleteuser",
data: {"userid":userId}
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({"userid": [userId]}),
url: getPath() + "/ajax/deleteuser",
});
$("#user-table").bootstrapTable("refresh");
}
@@ -949,6 +1352,5 @@ function user_handle (userId) {
function shorten_html(value, response) {
if(value) {
$(this).html("[...]");
// value.split('\n').slice(0, 2).join("") +
}
}

View File

@@ -19,6 +19,25 @@
{% if sort %}data-sortable="true" {% endif %}
data-visible="{{visiblility.get(parameter)}}"
data-formatter="bookCheckboxFormatter">
{% if parameter == "is_archived" %}
<div class="form-check">
<div>
<input type="radio" class="check_head" data-set="True" data-val="{{ parameter }}" name="options_archive_selected_books" id="false_archive_selected_books" data-name="archive_books" disabled>{{_('Archive selected books')}}
</div>
<div>
<input type="radio" class="check_head" data-set="False" data-val="{{ parameter }}" name="options_unarchive_selected_books" id="true_archive_selected_books" data-name="archive_books" disabled>{{_('Unarchive selected books')}}
</div>
</div>
{% elif parameter == "read_status" %}
<div class="form-check">
<div>
<input type="radio" class="check_head" data-set="True" data-val="{{ parameter }}" name="options_read_selected_books" id="false_read_selected_books" data-name="read_books" disabled>{{_('Mark selected books as read')}}
</div>
<div>
<input type="radio" class="check_head" data-set="False" data-val="{{ parameter }}" name="options_unread_selected_books" id="true_read_selected_books" data-name="read_books" disabled>{{_('Mark selected books as unread')}}</div>
</div>
</div>
{% endif %}
{{show_text}}
</th>
{%- endmacro %}
@@ -34,8 +53,15 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col-xs-12 col-sm-6">
<div class="row form-group">
<div class="btn btn-default disabled" id="merge_books" aria-disabled="true">{{_('Merge selected books')}}</div>
<div class="btn btn-default disabled" id="delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
<div class="btn btn-default disabled" id="merge_books" aria-disabled="true">
{{_('Merge selected books')}}
</div>
<div class="btn btn-default disabled mass_selection" id="book_delete_selection" aria-disabled="true">
{{_('Clear selections')}}
</div>
<div class="btn btn-default disabled" id="edit_selected_books" aria-disabled="true">
{{_('Edit selected books')}}
</div>
</div>
<div class="row form-group">
<div class="btn btn-default disabled" id="table_xchange" ><span class="glyphicon glyphicon-arrow-up"></span><span class="glyphicon glyphicon-arrow-down"></span>{{_('Exchange author and title')}}</div>
@@ -57,7 +83,7 @@
<thead>
<tr>
{% if current_user.role_edit() %}
<th data-field="state" data-checkbox="true" data-sortable="true"></th>
<th data-field="state" data-checkbox="true" data-visible="true" data-sortable="true"></th>
{% endif %}
<th data-field="id" id="id" data-visible="false" data-switchable="false"></th>
{{ text_table_row('title', _('Enter Title'),_('Title'), true, true) }}
@@ -97,14 +123,23 @@
{% endif %}
{% endfor %}
{% if current_user.role_delete_books() and current_user.role_edit()%}
<th data-align="right" data-formatter="EbookActions" data-switchable="false">{{_('Delete')}}</th>
<th data-align="right" data-formatter="EbookActions" data-switchable="false">
<div><div class="btn btn-default button_head disabled" aria-disabled="true">
{{_('Delete selected books')}}
</div></div>
<br>
{{_('Delete')}}
</th>
{% endif %}
</tr>
</thead>
</table>
{% endblock %}
{% block modal %}
{{ delete_book(current_user.role_delete_books()) }}
{{ delete_confirm_modal() }}
{{ change_confirm_modal() }}
{% if current_user.role_edit() %}
<div class="modal fade" id="mergeModal" role="dialog" aria-labelledby="metaMergeLabel">
<div class="modal-dialog">
@@ -123,15 +158,170 @@
<div class="text-left" id="merge_to"></div>
</div>
<div class="modal-footer">
<input id="merge_confirm" type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" id="merge_confirm" data-dismiss="modal">
<input id="merge_confirm" type="button" class="btn btn-danger" value="{{_('Merge')}}" name="merge_confirm" data-dismiss="modal">
<button id="merge_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
{% endif %}
<!--div class="modal fade" id="delete_selected_modal" role="dialog" aria-labelledby="metaDeleteSelectedLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body">
<p></p>
<div class="text-left">{{_('The following books will be deleted:')}}</div>
<p></p>
<div class="text-left" id="display-delete-selected-books"></div>
<div class="modal-footer">
<input id="delete_selected_confirm" type="button" class="btn btn-danger" value="{{_('Delete')}}" name="delete_selected_confirm" data-dismiss="modal">
<button id="delete_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
</div-->
<div class="modal fade" id="archive_selected_modal" role="dialog" aria-labelledby="metaArchiveSelectedLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body">
<p></p>
<div class="text-left">{{_('The following books will be archived:')}}</div>
<p></p>
<div class="text-left" id="display-archive-selected-books"></div>
<div class="modal-footer">
<input id="archive_selected_confirm" type="button" class="btn btn-danger" value="{{_('Archive')}}" name="archive_selected_confirm" data-dismiss="modal">
<button id="archive_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="unarchive_selected_modal" role="dialog" aria-labelledby="metaUnArchiveSelectedLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body">
<p></p>
<div class="text-left">{{_('The following books will be unarchived:')}}</div>
<p></p>
<div class="text-left" id="display-unarchive-selected-books"></div>
<div class="modal-footer">
<input id="unarchive_selected_confirm" type="button" class="btn btn-danger" value="{{_('Unarchive')}}" name="unarchive_selected_confirm" data-dismiss="modal">
<button id="unarchive_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="read_selected_modal" role="dialog" aria-labelledby="metaReadSelectedLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body">
<p></p>
<div class="text-left">{{_('The following books will be marked read:')}}</div>
<p></p>
<div class="text-left" id="display-read-selected-books"></div>
<div class="modal-footer">
<input id="read_selected_confirm" type="button" class="btn btn-danger" value="{{_('Mark as read')}}" name="read_selected_confirm" data-dismiss="modal">
<button id="read_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="unread_selected_modal" role="dialog" aria-labelledby="metaReadSelectedLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-center">
<span>{{_('Are you really sure?')}}</span>
</div>
<div class="modal-body">
<p></p>
<div class="text-left">{{_('The following books will be marked unread:')}}</div>
<p></p>
<div class="text-left" id="display-unread-selected-books"></div>
<div class="modal-footer">
<input id="unread_selected_confirm" type="button" class="btn btn-danger" value="{{_('Mark as unread')}}" name="unread_selected_confirm" data-dismiss="modal">
<button id="read_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="edit_selected_modal" role="dialog" aria-labelledby="metaEditSelectedLabel">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-info text-center">
<span>{{_('Edit Metadata')}}</span>
</div>
<div class="modal-body">
<div class="text-left">{{_('Edit the fields you want changed. Blank fields will be ignored:')}}</div>
<br>
Title:
<input class="form-control" id="title_input">
<p></p>
Title Sort:
<input class="form-control" id="title_sort_input">
<p></p>
Author Sort:
<input class="form-control" id="author_sort_input">
<p></p>
Authors:
<input class="form-control" id="authors_input">
<p></p>
Categories:
<input class="form-control" id="categories_input">
<p></p>
Series:
<input class="form-control" id="series_input">
<p></p>
Languages:
<input class="form-control" id="languages_input">
<p></p>
Publishers:
<input class="form-control" id="publishers_input">
<p></p>
Comments:
<input class="form-control" id="comments_input">
<p></p>
</div>
<div class="modal-footer">
<input id="edit_selected_confirm" type="button" class="btn btn-danger" value="{{_('Edit')}}" name="edit_selected_confirm" id="edit_selected_confirm" data-dismiss="modal">
<button id="edit_selected_abort" type="button" class="btn btn-default" data-dismiss="modal">{{_('Cancel')}}</button>
</div>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block js %}
<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-locale-all.min.js') }}"></script>

View File

@@ -53,7 +53,7 @@
data-visible="{{element.get(array_field)}}"
data-column="{{value.get(array_field)}}"
data-formatter="checkboxFormatter">
<div class="form-check">
<div class="form-check">
<div>
<input type="radio" class="check_head" data-set="false" data-val="{{value.get(array_field)}}" name="options_{{array_field}}" id="false_{{array_field}}" data-name="{{parameter}}" disabled>{{_('Deny')}}
</div>
@@ -121,7 +121,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="col-xs-12 col-sm-12">
<div class="row">
<div class="btn btn-default disabled" id="user_delete_selection" aria-disabled="true">{{_('Remove Selections')}}</div>
<div class="btn btn-default disabled mass_selection" id="user_delete_selection" aria-disabled="true">{{_('Clear selections')}}</div>
</div>
</div>
<table id="user-table" class="table table-no-bordered table-striped"

View File

@@ -1,10 +1,10 @@
# GDrive Integration
google-api-python-client>=2.73.00,<2.200.0
gevent>20.6.0,<24.12.0
greenlet>=0.4.17,<3.2.0
greenlet>=0.4.17,<3.3.0
httplib2>=0.9.2,<0.23.0
oauth2client>=4.0.0,<4.1.4
uritemplate>=3.0.0,<4.2.0
uritemplate>=3.0.0,<4.3.0
pyasn1-modules>=0.0.8,<0.7.0
pyasn1>=0.1.9,<0.7.0
PyDrive2>=1.15.0,<1.22.0

View File

@@ -71,10 +71,10 @@ content-type = "text/markdown"
gdrive = [
"google-api-python-client>=1.7.11,<2.200.0",
"gevent>20.6.0,<24.12.0",
"greenlet>=0.4.17,<3.2.0",
"greenlet>=0.4.17,<3.3.0",
"httplib2>=0.9.2,<0.23.0",
"oauth2client>=4.0.0,<4.1.4",
"uritemplate>=3.0.0,<4.2.0",
"uritemplate>=3.0.0,<4.3.0",
"pyasn1-modules>=0.0.8,<0.7.0",
"pyasn1>=0.1.9,<0.7.0",
"PyDrive2>=1.3.1,<1.22.0",

File diff suppressed because it is too large Load Diff