mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Basic metadata editing now works
Now, need to implement edit widgets for the different kinds of metadata fields
This commit is contained in:
parent
3451698bf3
commit
229dd92b59
@ -16,6 +16,7 @@ from calibre.srv.errors import HTTPBadRequest, HTTPForbidden, HTTPNotFound
|
|||||||
from calibre.srv.routes import endpoint, json, msgpack_or_json
|
from calibre.srv.routes import endpoint, json, msgpack_or_json
|
||||||
from calibre.srv.utils import get_db, get_library_data
|
from calibre.srv.utils import get_db, get_library_data
|
||||||
from calibre.utils.serialize import MSGPACK_MIME, json_loads, msgpack_loads
|
from calibre.utils.serialize import MSGPACK_MIME, json_loads, msgpack_loads
|
||||||
|
from calibre.srv.metadata import book_as_json
|
||||||
|
|
||||||
receive_data_methods = {'GET', 'POST'}
|
receive_data_methods = {'GET', 'POST'}
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ def cdb_run(ctx, rd, which, version):
|
|||||||
raise HTTPForbidden('Cannot use the command-line db interface with a user who has per library restrictions')
|
raise HTTPForbidden('Cannot use the command-line db interface with a user who has per library restrictions')
|
||||||
raw = rd.read()
|
raw = rd.read()
|
||||||
ct = rd.inheaders.get('Content-Type', all=True)
|
ct = rd.inheaders.get('Content-Type', all=True)
|
||||||
|
ct = {x.lower().partition(';')[0] for x in ct}
|
||||||
try:
|
try:
|
||||||
if MSGPACK_MIME in ct:
|
if MSGPACK_MIME in ct:
|
||||||
args = msgpack_loads(raw)
|
args = msgpack_loads(raw)
|
||||||
@ -106,3 +108,28 @@ def cdb_delete_book(ctx, rd, book_ids, library_id):
|
|||||||
db.remove_books(ids)
|
db.remove_books(ids)
|
||||||
books_deleted(ids)
|
books_deleted(ids)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@endpoint('/cdb/set-fields/{book_id}/{library_id=None}', types={'book_id': int},
|
||||||
|
needs_db_write=True, postprocess=msgpack_or_json, methods=receive_data_methods, cache_control='no-cache')
|
||||||
|
def cdb_set_fields(ctx, rd, book_id, library_id):
|
||||||
|
db = get_db(ctx, rd, library_id)
|
||||||
|
if ctx.restriction_for(rd, db):
|
||||||
|
raise HTTPForbidden('Cannot use the set fields interface with a user who has per library restrictions')
|
||||||
|
raw = rd.read()
|
||||||
|
ct = rd.inheaders.get('Content-Type', all=True)
|
||||||
|
ct = {x.lower().partition(';')[0] for x in ct}
|
||||||
|
try:
|
||||||
|
if MSGPACK_MIME in ct:
|
||||||
|
data = msgpack_loads(raw)
|
||||||
|
elif 'application/json' in ct:
|
||||||
|
data = json_loads(raw)
|
||||||
|
else:
|
||||||
|
raise HTTPBadRequest('Only JSON or msgpack requests are supported')
|
||||||
|
changes, loaded_book_ids = data['changes'], frozenset(map(int, data['loaded_book_ids']))
|
||||||
|
except Exception:
|
||||||
|
raise HTTPBadRequest('Invalid encoded data')
|
||||||
|
dirtied = set()
|
||||||
|
for field, value in changes.iteritems():
|
||||||
|
dirtied |= db.set_field(field, {book_id: value})
|
||||||
|
return {bid: book_as_json(db, book_id) for bid in (dirtied & loaded_book_ids) | {book_id}}
|
||||||
|
@ -6,15 +6,20 @@ import traceback
|
|||||||
from elementmaker import E
|
from elementmaker import E
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
|
|
||||||
|
from ajax import ajax_send
|
||||||
from book_list.book_details import (
|
from book_list.book_details import (
|
||||||
basic_table_rules, fetch_metadata, field_sorter, no_book, report_load_failure
|
basic_table_rules, fetch_metadata, field_sorter, no_book, report_load_failure
|
||||||
)
|
)
|
||||||
from book_list.library_data import book_metadata, library_data, load_status
|
from book_list.library_data import (
|
||||||
|
book_metadata, current_library_id, library_data, load_status, loaded_book_ids,
|
||||||
|
set_book_metadata
|
||||||
|
)
|
||||||
from book_list.router import back
|
from book_list.router import back
|
||||||
from book_list.top_bar import create_top_bar, set_title
|
from book_list.top_bar import create_top_bar, set_title
|
||||||
from book_list.ui import set_panel_handler, show_panel
|
from book_list.ui import set_panel_handler, show_panel
|
||||||
from date import format_date
|
from date import format_date
|
||||||
from dom import add_extra_css, build_rule, clear, svgicon
|
from dom import add_extra_css, build_rule, clear, svgicon
|
||||||
|
from modals import error_dialog
|
||||||
from session import get_interface_data
|
from session import get_interface_data
|
||||||
from utils import (
|
from utils import (
|
||||||
conditional_timeout, fmt_sidx, parse_url_params, safe_set_inner_html
|
conditional_timeout, fmt_sidx, parse_url_params, safe_set_inner_html
|
||||||
@ -23,6 +28,9 @@ from widgets import create_button
|
|||||||
|
|
||||||
CLASS_NAME = 'edit-metadata-panel'
|
CLASS_NAME = 'edit-metadata-panel'
|
||||||
IGNORED_FIELDS = {'formats', 'sort', 'uuid', 'id', 'urls_from_identifiers', 'lang_names', 'last_modified', 'path', 'marked', 'size', 'ondevice', 'cover', 'au_map', 'isbn'}
|
IGNORED_FIELDS = {'formats', 'sort', 'uuid', 'id', 'urls_from_identifiers', 'lang_names', 'last_modified', 'path', 'marked', 'size', 'ondevice', 'cover', 'au_map', 'isbn'}
|
||||||
|
value_to_json = None
|
||||||
|
changes = {}
|
||||||
|
has_changes = False
|
||||||
|
|
||||||
add_extra_css(def():
|
add_extra_css(def():
|
||||||
sel = '.' + CLASS_NAME + ' '
|
sel = '.' + CLASS_NAME + ' '
|
||||||
@ -34,6 +42,13 @@ add_extra_css(def():
|
|||||||
return style
|
return style
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolved_metadata(mi, field):
|
||||||
|
if Object.prototype.hasOwnProperty.call(changes, field):
|
||||||
|
return changes[field]
|
||||||
|
return mi[field]
|
||||||
|
|
||||||
|
|
||||||
def truncated_html(val):
|
def truncated_html(val):
|
||||||
ans = val.replace(/<[^>]+>/g, '')
|
ans = val.replace(/<[^>]+>/g, '')
|
||||||
if ans.length > 40:
|
if ans.length > 40:
|
||||||
@ -41,6 +56,7 @@ def truncated_html(val):
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def onsubmit_field(get_value, container_id, book_id, field):
|
def onsubmit_field(get_value, container_id, book_id, field):
|
||||||
c = document.getElementById(container_id)
|
c = document.getElementById(container_id)
|
||||||
if not c:
|
if not c:
|
||||||
@ -56,10 +72,16 @@ def onsubmit_field(get_value, container_id, book_id, field):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def proceed():
|
def proceed():
|
||||||
|
nonlocal has_changes
|
||||||
clear(d)
|
clear(d)
|
||||||
d.appendChild(E.div(style='margin: 1ex 1rem', _('Contacting server, please wait') + '…'))
|
d.appendChild(E.div(style='margin: 1ex 1rem', _('Contacting server, please wait') + '…'))
|
||||||
value
|
jval = value_to_json(value)
|
||||||
# TODO: ajax to server with field and value and get updated metadata
|
changes[field] = jval
|
||||||
|
has_changes = True
|
||||||
|
show_book(container_id, book_id)
|
||||||
|
on_close(container_id)
|
||||||
|
|
||||||
|
|
||||||
window.setTimeout(proceed, 0) # needed to avoid console error about form submission failing because form is removed from DOM in onsubmit handler
|
window.setTimeout(proceed, 0) # needed to avoid console error about form submission failing because form is removed from DOM in onsubmit handler
|
||||||
|
|
||||||
|
|
||||||
@ -80,16 +102,20 @@ def line_edit_get_value(container):
|
|||||||
|
|
||||||
|
|
||||||
def simple_line_edit(container_id, book_id, field, fm, div, mi):
|
def simple_line_edit(container_id, book_id, field, fm, div, mi):
|
||||||
le = E.input(type='text')
|
nonlocal value_to_json
|
||||||
le.value = mi[field] or ''
|
|
||||||
name = fm.name or field
|
name = fm.name or field
|
||||||
|
le = E.input(type='text', name=name.replace('#', '_c_'), autocomplete=True)
|
||||||
|
le.value = resolved_metadata(mi, field) or ''
|
||||||
form = create_form(le, line_edit_get_value, container_id, book_id, field)
|
form = create_form(le, line_edit_get_value, container_id, book_id, field)
|
||||||
div.appendChild(E.div(style='margin: 0.5ex 1rem', _('Edit the "{}" below').format(name)))
|
div.appendChild(E.div(style='margin: 0.5ex 1rem', _('Edit the "{}" below').format(name)))
|
||||||
div.appendChild(E.div(style='margin: 0.5ex 1rem', form))
|
div.appendChild(E.div(style='margin: 0.5ex 1rem', form))
|
||||||
le.focus(), le.select()
|
le.focus(), le.select()
|
||||||
|
value_to_json = def(x):
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
def edit_field(container_id, book_id, field):
|
def edit_field(container_id, book_id, field):
|
||||||
|
nonlocal value_to_json
|
||||||
fm = library_data.field_metadata[field]
|
fm = library_data.field_metadata[field]
|
||||||
c = document.getElementById(container_id)
|
c = document.getElementById(container_id)
|
||||||
mi = book_metadata(book_id)
|
mi = book_metadata(book_id)
|
||||||
@ -100,6 +126,9 @@ def edit_field(container_id, book_id, field):
|
|||||||
d.previousSibling.style.display = 'none'
|
d.previousSibling.style.display = 'none'
|
||||||
clear(d)
|
clear(d)
|
||||||
simple_line_edit(container_id, book_id, field, fm, d, mi)
|
simple_line_edit(container_id, book_id, field, fm, d, mi)
|
||||||
|
if field is 'title':
|
||||||
|
value_to_json = def(x):
|
||||||
|
return x or _('Untitled')
|
||||||
|
|
||||||
|
|
||||||
def render_metadata(mi, table, container_id, book_id): # {{{
|
def render_metadata(mi, table, container_id, book_id): # {{{
|
||||||
@ -219,7 +248,7 @@ def render_metadata(mi, table, container_id, book_id): # {{{
|
|||||||
if val:
|
if val:
|
||||||
ifield = field + '_index'
|
ifield = field + '_index'
|
||||||
try:
|
try:
|
||||||
ival = float(mi[ifield])
|
ival = float(resolved_metadata(mi, ifield))
|
||||||
except Exception:
|
except Exception:
|
||||||
ival = 1.0
|
ival = 1.0
|
||||||
ival = fmt_sidx(ival, use_roman=interface_data.use_roman_numerals_for_series_number)
|
ival = fmt_sidx(ival, use_roman=interface_data.use_roman_numerals_for_series_number)
|
||||||
@ -232,7 +261,7 @@ def render_metadata(mi, table, container_id, book_id): # {{{
|
|||||||
def process_field(field, fm):
|
def process_field(field, fm):
|
||||||
name = fm.name or field
|
name = fm.name or field
|
||||||
datatype = fm.datatype
|
datatype = fm.datatype
|
||||||
val = mi[field]
|
val = resolved_metadata(mi, field)
|
||||||
if field is 'comments' or datatype is 'comments':
|
if field is 'comments' or datatype is 'comments':
|
||||||
add_row(name, truncated_html(val or ''))
|
add_row(name, truncated_html(val or ''))
|
||||||
return
|
return
|
||||||
@ -289,18 +318,58 @@ def render_metadata(mi, table, container_id, book_id): # {{{
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
def changes_submitted(container_id, book_id, end_type, xhr, ev):
|
||||||
|
nonlocal changes, has_changes
|
||||||
|
changes = {}
|
||||||
|
has_changes = False
|
||||||
|
if end_type is 'abort':
|
||||||
|
on_close(container_id)
|
||||||
|
return
|
||||||
|
if end_type is not 'load':
|
||||||
|
error_dialog(_('Failed to update metadata on server'), _(
|
||||||
|
'Updating metadata for the book: {} failed.').format(book_id), xhr.error_html)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
dirtied = JSON.parse(xhr.responseText)
|
||||||
|
except Exception as err:
|
||||||
|
error_dialog(_('Could not update metadata for book'), _('Server returned an invalid response'), err.toString())
|
||||||
|
return
|
||||||
|
|
||||||
|
for bid in dirtied:
|
||||||
|
set_book_metadata(bid, dirtied[book_id])
|
||||||
|
on_close(container_id)
|
||||||
|
|
||||||
|
|
||||||
|
def submit_changes(container_id, book_id):
|
||||||
|
c = document.getElementById(container_id)
|
||||||
|
d = c.querySelector('div[data-ctype="show"]')
|
||||||
|
clear(d)
|
||||||
|
d.appendChild(E.div(style='margin: 1ex 1rem', _('Uploading changes to server, please wait...')))
|
||||||
|
data = {'changes': changes, 'loaded_book_ids': loaded_book_ids()}
|
||||||
|
ajax_send(
|
||||||
|
f'cdb/set-fields/{book_id}/{current_library_id()}', data, changes_submitted.bind(None, container_id, book_id))
|
||||||
|
|
||||||
|
|
||||||
def show_book(container_id, book_id):
|
def show_book(container_id, book_id):
|
||||||
container = document.getElementById(container_id)
|
container = document.getElementById(container_id)
|
||||||
mi = book_metadata(book_id)
|
mi = book_metadata(book_id)
|
||||||
if not mi or not container:
|
if not container or not mi:
|
||||||
return
|
return
|
||||||
div = container.querySelector('div[data-ctype="show"]')
|
div = container.querySelector('div[data-ctype="show"]')
|
||||||
if not div:
|
if not div:
|
||||||
return
|
return
|
||||||
|
clear(div)
|
||||||
|
if has_changes:
|
||||||
|
b = create_button(_('Apply changes'), action=submit_changes.bind(None, container_id, book_id))
|
||||||
|
div.appendChild(E.div(style='margin: 1ex 1rem', b))
|
||||||
|
else:
|
||||||
div.appendChild(E.div(style='margin: 1ex 1rem', _(
|
div.appendChild(E.div(style='margin: 1ex 1rem', _(
|
||||||
'Tap any field below to edit it')))
|
'Tap any field below to edit it')))
|
||||||
div.appendChild(E.table(class_='metadata'))
|
div.appendChild(E.table(class_='metadata'))
|
||||||
render_metadata(mi, div.lastChild, container_id, book_id)
|
render_metadata(mi, div.lastChild, container_id, book_id)
|
||||||
|
if has_changes:
|
||||||
|
b = create_button(_('Apply changes'), action=submit_changes.bind(None, container_id, book_id))
|
||||||
|
div.appendChild(E.div(style='margin: 1ex 1rem', b))
|
||||||
|
|
||||||
|
|
||||||
def on_close(container_id):
|
def on_close(container_id):
|
||||||
@ -319,6 +388,9 @@ def on_close(container_id):
|
|||||||
|
|
||||||
|
|
||||||
def proceed_after_succesful_fetch_metadata(container_id, book_id):
|
def proceed_after_succesful_fetch_metadata(container_id, book_id):
|
||||||
|
nonlocal changes, has_changes
|
||||||
|
changes = {}
|
||||||
|
has_changes = False
|
||||||
container = document.getElementById(container_id)
|
container = document.getElementById(container_id)
|
||||||
mi = book_metadata(book_id)
|
mi = book_metadata(book_id)
|
||||||
if not mi or not container:
|
if not mi or not container:
|
||||||
|
@ -166,6 +166,10 @@ def set_book_metadata(book_id, value):
|
|||||||
library_data.metadata[book_id] = value
|
library_data.metadata[book_id] = value
|
||||||
|
|
||||||
|
|
||||||
|
def loaded_book_ids():
|
||||||
|
return Object.keys(library_data.metadata)
|
||||||
|
|
||||||
|
|
||||||
def force_refresh_on_next_load():
|
def force_refresh_on_next_load():
|
||||||
library_data.force_refresh = True
|
library_data.force_refresh = True
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user