Content server: Implement deleting of books via the web interface

This commit is contained in:
Kovid Goyal 2018-01-25 20:35:55 +05:30
parent ce22a0da00
commit 27a062cb19
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 191 additions and 11 deletions

View File

@ -11,6 +11,7 @@ import sys
from calibre import as_unicode
from calibre.constants import is_running_from_develop, isosx, iswindows, plugins
from calibre.db.legacy import LibraryDatabase
from calibre.db.delete_service import shutdown as shutdown_delete_service
from calibre.srv.bonjour import BonJour
from calibre.srv.handler import Handler
from calibre.srv.http_response import create_http_handler
@ -220,4 +221,7 @@ def main(args=sys.argv):
# Needed for dynamic cover generation, which uses Qt for drawing
from calibre.gui2 import ensure_app, load_builtin_fonts
ensure_app(), load_builtin_fonts()
server.serve_forever()
try:
server.serve_forever()
finally:
shutdown_delete_service()

View File

@ -82,6 +82,17 @@ def list_duplicate_book(container, container_id, job_id, data, file):
container.appendChild(b)
def write_access_error(msg, xhr):
html = msg + '<br>'
if xhr.status is 403:
un = get_interface_data().username
if un:
html += _('You are not allowed to make changes to the library') + '<hr>'
else:
html += _('You must be logged in to make changes to the library') + '<hr>'
return html + '<br>' + xhr.error_html
def on_complete(container_id, job_id, end_type, xhr, ev):
idx = state.transfers.indexOf(xhr)
if idx > -1:
@ -100,14 +111,8 @@ def on_complete(container_id, job_id, end_type, xhr, ev):
elif end_type is 'abort':
return
else:
html = _('Failed to upload the file: {}').format(this.name) + '<br>'
if xhr.status is 403:
un = get_interface_data().username
if un:
html += _('You are not allowed to make changes to the library') + '<hr>'
else:
html += _('You must be logged in to make changes to the library') + '<hr>'
safe_set_inner_html(container, html + '<br>' + xhr.error_html)
html = write_access_error(_('Failed to upload the file: {}').format(this.name), xhr)
safe_set_inner_html(container, html)
def fake_send(container_id, job_id):
@ -123,7 +128,7 @@ def send_file(file, container_id, job_id, add_duplicates):
lid = loaded_books_query().library_id
ad = 'y' if add_duplicates else 'n'
xhr = ajax_send_file(
f'/cdb/add-book/{job_id}/{ad}/{encodeURIComponent(file.name)}/{lid}',
f'cdb/add-book/{job_id}/{ad}/{encodeURIComponent(file.name)}/{lid}',
file, on_complete.bind(file, container_id, job_id), on_progress.bind(None, container_id, job_id))
state.transfers.push(xhr)
return xhr

View File

@ -7,6 +7,7 @@ from elementmaker import E
from gettext import gettext as _
from ajax import ajax, encode_query_component
from book_list.delete_book import start_delete_book
from book_list.globals import get_session_data
from book_list.item_list import create_item, create_item_list
from book_list.library_data import (
@ -15,7 +16,7 @@ from book_list.library_data import (
)
from book_list.router import back, home, open_book
from book_list.theme import get_color, get_font_size
from book_list.top_bar import add_button, create_top_bar, set_title, clear_buttons
from book_list.top_bar import add_button, clear_buttons, create_top_bar, set_title
from book_list.ui import query_as_href, set_panel_handler, show_panel
from book_list.views import search_query_for
from date import format_date
@ -415,6 +416,7 @@ def render_book(container_id, book_id):
if not c:
return
metadata = book_metadata(book_id)
render_book.title = metadata.title
set_title(c, metadata.title)
authors = metadata.authors.join(' & ') if metadata.authors else _('Unknown')
alt = _('{} by {}').format(metadata.title, authors)
@ -453,6 +455,7 @@ def add_top_bar_buttons(container_id):
container = document.getElementById(container_id)
if container:
clear_buttons(container)
add_button(container, 'trash', action=delete_book, tooltip=_('Delete this book'))
book_id = parse_url_params().book_id
if book_id is '0':
add_button(container, 'random', def(): fetch_metadata(container_id, 0);)
@ -630,6 +633,10 @@ def search_internet(container_id):
))
def delete_book():
start_delete_book(current_library_id(), render_book.book_id, render_book.title or _('Unknown'))
set_panel_handler('book_details', init)
set_panel_handler('book_details^more_actions', create_more_actions_panel)
set_panel_handler('book_details^search_internet', search_internet)

View File

@ -0,0 +1,110 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals
from elementmaker import E
from gettext import gettext as _
from ajax import ajax
from book_list.add import write_access_error
from book_list.library_data import book_after, force_refresh_on_next_load
from book_list.router import back
from book_list.ui import show_panel
from dom import clear
from modals import create_custom_dialog
from read_book.db import get_db
from utils import safe_set_inner_html
from widgets import create_button
def delete_from_cache(library_id, book_id, title):
db = get_db()
db.delete_books_matching(library_id, book_id)
def do_delete_from_library(parent, close_modal, library_id, book_id, title):
parent.appendChild(E.div(_('Deleting {0} from library, please wait...').format(title)))
def oncomplete(end_type, xhr, ev):
if end_type is 'load' or end_type is 'abort':
close_modal()
if end_type is 'load':
force_refresh_on_next_load()
next_book_id = book_after(book_id)
if next_book_id:
show_panel('book_details', {'book_id': str(next_book_id), 'library_id': library_id}, True)
else:
back()
return
clear(parent)
msg = E.div()
safe_set_inner_html(msg, write_access_error(_('Failed to delete {0}, with error:').format(title), xhr))
parent.appendChild(E.div(
msg,
E.div(class_='button-box',
create_button(_('Close'), None, close_modal, True)
)
))
ajax(f'cdb/delete-books/{book_id}/{library_id}', oncomplete, method='POST').send()
def confirm_delete_from_library(library_id, book_id, title):
create_custom_dialog(_('Are you sure?'), def(parent, close_modal):
def action(doit):
if doit:
clear(parent)
do_delete_from_library(parent, close_modal, library_id, book_id, title)
else:
close_modal()
msg = _('This will <b>permanently delete</b> <i>{0}</i> from your calibre library. Are you sure?').format(title)
m = E.div()
safe_set_inner_html(m, msg)
parent.appendChild(E.div(
m,
E.div(class_='button-box',
create_button(_('OK'), None, action.bind(None, True)),
'\xa0',
create_button(_('Cancel'), None, action.bind(None, False), highlight=True),
)
))
)
def choose_which_delete(library_id, book_id, title):
create_custom_dialog(_('Delete from?'), def(parent, close_modal):
def action(from_cache, from_library):
close_modal()
if from_cache:
delete_from_cache(library_id, book_id, title)
if from_library:
confirm_delete_from_library(library_id, book_id, title)
parent.appendChild(E.div(
E.div(_('{0} is available both in the browser cache for offline reading and in'
' your calibre library. Where do you want to delete it from?').format(title)),
E.div(class_='button-box',
create_button(_('Cache'), None, action.bind(None, True, False)),
'\xa0',
create_button(_('Library and cache'), None, action.bind(None, True, True)),
)
))
)
def delete_book_stage2(library_id, book_id, title, has_offline_copies):
if has_offline_copies:
choose_which_delete(library_id, book_id, title)
else:
confirm_delete_from_library(library_id, book_id, title)
def start_delete_book(library_id, book_id, title):
book_id = int(book_id)
db = get_db()
db.has_book_matching(library_id, book_id, delete_book_stage2.bind(None, library_id, book_id, title))

View File

@ -97,6 +97,16 @@ def current_book_ids():
return library_data.previous_book_ids.concat(library_data.search_result.book_ids)
def book_after(book_id):
ids = current_book_ids()
idx = ids.indexOf(int(book_id))
if idx > -1:
if idx < ids.length - 1:
return ids[idx + 1]
if idx > 0: # wrap around
return ids[0]
def on_data_loaded(end_type, xhr, ev):
load_status.current_fetch = None
def bad_load(msg):

View File

@ -348,6 +348,25 @@ class DB:
if cursor:
cursor.continue()
def has_book_matching(self, library_id, book_id, proceed):
# Should really be using a multiEntry index to avoid iterating over all
# books in JS, but since the number of stored books is not that large,
# I can't be bothered.
c = self.idb.transaction(['books'], 'readonly').objectStore('books').index('recent_index').openCursor(None, 'prev')
c.onerror = def(event):
proceed(False)
book_id = int(book_id)
c.onsuccess = def (ev):
cursor = ev.target.result
if cursor:
book = cursor.value
if book.key[0] is library_id and book.key[1] is book_id:
proceed(True)
return
cursor.continue()
else:
proceed(False)
def delete_book(self, book, proceed):
c = self.idb.transaction(['books', 'files'], 'readwrite')
files = c.objectStore('files')
@ -366,6 +385,31 @@ class DB:
books.delete(book.key)
next_step()
def delete_books_matching(self, library_id, book_id, proceed):
c = self.idb.transaction(['books'], 'readonly').objectStore('books').index('recent_index').openCursor(None, 'prev')
c.onerror = def(event):
pass
book_id = int(book_id)
matches = v'[]'
def delete_all():
if matches.length:
book = matches.pop()
self.delete_book(book, delete_all)
else:
if proceed:
proceed()
c.onsuccess = def (ev):
cursor = ev.target.result
if cursor:
book = cursor.value
if book.key[0] is library_id and book.key[1] is book_id:
matches.push(book)
cursor.continue()
else:
delete_all()
def get_db(callback, show_read_book_error):
if not get_db.ans: