mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Make item delete and rename take VLs into consideration
This commit is contained in:
parent
fd5dad9bce
commit
5557660bbd
@ -1564,24 +1564,60 @@ class Cache(object):
|
|||||||
return val_map
|
return val_map
|
||||||
|
|
||||||
@write_api
|
@write_api
|
||||||
def rename_items(self, field, item_id_to_new_name_map, change_index=True):
|
def rename_items(self, field, item_id_to_new_name_map, change_index=True,
|
||||||
|
restrict_to_book_ids=None):
|
||||||
'''
|
'''
|
||||||
Rename items from a many-one or many-many field such as tags or series.
|
Rename items from a many-one or many-many field such as tags or series.
|
||||||
|
|
||||||
:param change_index: When renaming in a series-like field also change the series_index values.
|
:param change_index: When renaming in a series-like field also change the series_index values.
|
||||||
|
:param restrict_to_book_ids: A list of books for which the rename is to be performed. None if the entire library
|
||||||
'''
|
'''
|
||||||
|
|
||||||
f = self.fields[field]
|
f = self.fields[field]
|
||||||
try:
|
|
||||||
func = f.table.rename_item
|
|
||||||
except AttributeError:
|
|
||||||
raise ValueError('Cannot rename items for one-one fields: %s' % field)
|
|
||||||
affected_books = set()
|
affected_books = set()
|
||||||
moved_books = set()
|
|
||||||
id_map = {}
|
|
||||||
try:
|
try:
|
||||||
sv = f.metadata['is_multiple']['ui_to_list']
|
sv = f.metadata['is_multiple']['ui_to_list']
|
||||||
except (TypeError, KeyError, AttributeError):
|
except (TypeError, KeyError, AttributeError):
|
||||||
sv = None
|
sv = None
|
||||||
|
|
||||||
|
if restrict_to_book_ids:
|
||||||
|
# We have a VL. Only change the item name for those books
|
||||||
|
rtb_set = frozenset(restrict_to_book_ids)
|
||||||
|
id_map = {}
|
||||||
|
for old_id, new_name in item_id_to_new_name_map.iteritems():
|
||||||
|
new_names = tuple(x.strip() for x in new_name.split(sv)) if sv else (new_name,)
|
||||||
|
# Get a list of books in the VL with the item
|
||||||
|
books_to_process = f.books_for(old_id) & rtb_set
|
||||||
|
# This should never be empty, but ...
|
||||||
|
if books_to_process:
|
||||||
|
affected_books.update(books_to_process)
|
||||||
|
newvals = {}
|
||||||
|
for book in books_to_process:
|
||||||
|
# Get the current values, remove the one being renamed, then add
|
||||||
|
# the new value(s) back
|
||||||
|
vals = self._field_for(field, book)
|
||||||
|
# Check for is_multiple
|
||||||
|
if isinstance(vals, tuple):
|
||||||
|
vals = set(vals)
|
||||||
|
# Don't need to worry about case here because we
|
||||||
|
# are fetching its one-true spelling
|
||||||
|
vals.remove(self.get_item_name(field, old_id))
|
||||||
|
# This can put the name back with a different case
|
||||||
|
vals.update(new_names)
|
||||||
|
newvals[book] = vals
|
||||||
|
else:
|
||||||
|
newvals[book] = new_names[0]
|
||||||
|
# Allow case changes
|
||||||
|
self._set_field(field, newvals)
|
||||||
|
id_map[old_id] = self.get_item_id(field, new_names[0])
|
||||||
|
return affected_books, id_map
|
||||||
|
|
||||||
|
try:
|
||||||
|
func = f.table.rename_item
|
||||||
|
except AttributeError:
|
||||||
|
raise ValueError('Cannot rename items for one-one fields: %s' % field)
|
||||||
|
moved_books = set()
|
||||||
|
id_map = {}
|
||||||
for item_id, new_name in item_id_to_new_name_map.iteritems():
|
for item_id, new_name in item_id_to_new_name_map.iteritems():
|
||||||
new_names = tuple(x.strip() for x in new_name.split(sv)) if sv else (new_name,)
|
new_names = tuple(x.strip() for x in new_name.split(sv)) if sv else (new_name,)
|
||||||
books, new_id = func(item_id, new_names[0], self.backend)
|
books, new_id = func(item_id, new_names[0], self.backend)
|
||||||
@ -1606,10 +1642,11 @@ class Cache(object):
|
|||||||
return affected_books, id_map
|
return affected_books, id_map
|
||||||
|
|
||||||
@write_api
|
@write_api
|
||||||
def remove_items(self, field, item_ids):
|
def remove_items(self, field, item_ids, restrict_to_book_ids=None):
|
||||||
''' Delete all items in the specified field with the specified ids. Returns the set of affected book ids. '''
|
''' Delete all items in the specified field with the specified ids. Returns the set of affected book ids. '''
|
||||||
field = self.fields[field]
|
field = self.fields[field]
|
||||||
affected_books = field.table.remove_items(item_ids, self.backend)
|
affected_books = field.table.remove_items(item_ids, self.backend,
|
||||||
|
restrict_to_book_ids=restrict_to_book_ids)
|
||||||
if affected_books:
|
if affected_books:
|
||||||
if hasattr(field, 'index_field'):
|
if hasattr(field, 'index_field'):
|
||||||
self._set_field(field.index_field.name, {bid:1.0 for bid in affected_books})
|
self._set_field(field.index_field.name, {bid:1.0 for bid in affected_books})
|
||||||
|
@ -820,8 +820,9 @@ for field in (
|
|||||||
|
|
||||||
for field in ('authors', 'tags', 'publisher'):
|
for field in ('authors', 'tags', 'publisher'):
|
||||||
def renamer(field):
|
def renamer(field):
|
||||||
def func(self, old_id, new_name):
|
def func(self, old_id, new_name, restrict_to_book_ids=None):
|
||||||
id_map = self.new_api.rename_items(field, {old_id:new_name})[1]
|
id_map = self.new_api.rename_items(field, {old_id:new_name},
|
||||||
|
restrict_to_book_ids=restrict_to_book_ids)[1]
|
||||||
if field == 'authors':
|
if field == 'authors':
|
||||||
return id_map[old_id]
|
return id_map[old_id]
|
||||||
return func
|
return func
|
||||||
@ -877,8 +878,8 @@ for field in ('author', 'tag', 'series'):
|
|||||||
for field in ('publisher', 'series', 'tag'):
|
for field in ('publisher', 'series', 'tag'):
|
||||||
def getter(field):
|
def getter(field):
|
||||||
fname = 'tags' if field == 'tag' else field
|
fname = 'tags' if field == 'tag' else field
|
||||||
def func(self, item_id):
|
def func(self, item_id, restrict_to_book_ids=None):
|
||||||
self.new_api.remove_items(fname, (item_id,))
|
self.new_api.remove_items(fname, (item_id,), restrict_to_book_ids=restrict_to_book_ids)
|
||||||
return func
|
return func
|
||||||
setattr(LibraryDatabase, 'delete_%s_using_id' % field, MT(getter(field)))
|
setattr(LibraryDatabase, 'delete_%s_using_id' % field, MT(getter(field)))
|
||||||
# }}}
|
# }}}
|
||||||
|
@ -265,8 +265,37 @@ class ManyToOneTable(Table):
|
|||||||
[(x,) for x in clean])
|
[(x,) for x in clean])
|
||||||
return clean
|
return clean
|
||||||
|
|
||||||
def remove_items(self, item_ids, db):
|
def remove_items(self, item_ids, db, restrict_to_book_ids=None):
|
||||||
affected_books = set()
|
affected_books = set()
|
||||||
|
|
||||||
|
if restrict_to_book_ids:
|
||||||
|
rtb_set = frozenset(restrict_to_book_ids)
|
||||||
|
items_to_process_normally = set()
|
||||||
|
# Check if all the books with the item are in the restriction. If
|
||||||
|
# so, process them normally
|
||||||
|
for item_id in item_ids:
|
||||||
|
books_to_process = self.col_book_map.get(item_id, set())
|
||||||
|
books_not_to_delete = books_to_process - rtb_set
|
||||||
|
if books_not_to_delete:
|
||||||
|
# Some books not in restriction. Must do special processing
|
||||||
|
books_to_delete = books_to_process & rtb_set
|
||||||
|
# remove the books from the old id maps
|
||||||
|
self.col_book_map[item_id] = books_not_to_delete
|
||||||
|
for book_id in books_to_delete:
|
||||||
|
self.book_col_map.pop(book_id, None)
|
||||||
|
# Delete links to the affected books from the link table. As
|
||||||
|
# this is a many-to-one mapping we know that we can delete
|
||||||
|
# links without checking the item ID
|
||||||
|
db.executemany('DELETE FROM {0} WHERE {1}=?'.format(self.link_table,
|
||||||
|
'book'), books_to_delete)
|
||||||
|
affected_books |= books_to_delete
|
||||||
|
else:
|
||||||
|
# Process normally any items where the VL was not significant
|
||||||
|
items_to_process_normally.add(item_id)
|
||||||
|
if items_to_process_normally:
|
||||||
|
affected_books |= self.remove_items(items_to_process_normally, db)
|
||||||
|
return affected_books
|
||||||
|
|
||||||
for item_id in item_ids:
|
for item_id in item_ids:
|
||||||
val = self.id_map.pop(item_id, null)
|
val = self.id_map.pop(item_id, null)
|
||||||
if val is null:
|
if val is null:
|
||||||
@ -373,8 +402,38 @@ class ManyToManyTable(ManyToOneTable):
|
|||||||
[(x,) for x in clean])
|
[(x,) for x in clean])
|
||||||
return clean
|
return clean
|
||||||
|
|
||||||
def remove_items(self, item_ids, db):
|
def remove_items(self, item_ids, db, restrict_to_book_ids=None):
|
||||||
affected_books = set()
|
affected_books = set()
|
||||||
|
if restrict_to_book_ids:
|
||||||
|
rtb_set = frozenset(restrict_to_book_ids)
|
||||||
|
items_to_process_normally = set()
|
||||||
|
# Check if all the books with the item are in the restriction. If
|
||||||
|
# so, process them normally
|
||||||
|
for item_id in item_ids:
|
||||||
|
books_to_process = self.col_book_map.get(item_id, set())
|
||||||
|
books_not_to_delete = books_to_process - rtb_set
|
||||||
|
if books_not_to_delete:
|
||||||
|
# Some books not in restriction. Must do special processing
|
||||||
|
books_to_delete = books_to_process & rtb_set
|
||||||
|
# remove the books from the old id maps
|
||||||
|
self.col_book_map[item_id] = books_not_to_delete
|
||||||
|
for book_id in books_to_delete:
|
||||||
|
self.book_col_map[book_id] = tuple(
|
||||||
|
x for x in self.book_col_map.get(book_id, ()) if x != item_id)
|
||||||
|
affected_books |= books_to_delete
|
||||||
|
else:
|
||||||
|
items_to_process_normally.add(item_id)
|
||||||
|
# Delete book/item pairs from the link table. We don't need to do
|
||||||
|
# anything with the main table because books with the old ID are
|
||||||
|
# still in the library.
|
||||||
|
db.executemany('DELETE FROM {0} WHERE {1}=? and {2}=?'.format(
|
||||||
|
self.link_table, 'book', self.metadata['link_column']),
|
||||||
|
[(b, i) for b in affected_books for i in item_ids])
|
||||||
|
# Take care of any items where the VL was not significant
|
||||||
|
if items_to_process_normally:
|
||||||
|
affected_books |= self.remove_items(items_to_process_normally, db)
|
||||||
|
return affected_books
|
||||||
|
|
||||||
for item_id in item_ids:
|
for item_id in item_ids:
|
||||||
val = self.id_map.pop(item_id, null)
|
val = self.id_map.pop(item_id, null)
|
||||||
if val is null:
|
if val is null:
|
||||||
|
@ -855,6 +855,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.drag_drop_finished.emit(ids)
|
self.drag_drop_finished.emit(ids)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
def get_book_ids_to_use(self):
|
||||||
|
if self.db.data.get_base_restriction() or self.db.data.get_search_restriction():
|
||||||
|
return self.db.search('', return_matches=True, sort_results=False)
|
||||||
|
return None
|
||||||
|
|
||||||
def _get_category_nodes(self, sort):
|
def _get_category_nodes(self, sort):
|
||||||
'''
|
'''
|
||||||
Called by __init__. Do not directly call this method.
|
Called by __init__. Do not directly call this method.
|
||||||
@ -863,21 +868,17 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.categories = {}
|
self.categories = {}
|
||||||
|
|
||||||
# Get the categories
|
# Get the categories
|
||||||
if self.db.data.get_base_restriction() or self.db.data.get_search_restriction():
|
try:
|
||||||
try:
|
data = self.db.new_api.get_categories(sort=sort,
|
||||||
data = self.db.new_api.get_categories(sort=sort,
|
icon_map=self.category_icon_map,
|
||||||
icon_map=self.category_icon_map,
|
book_ids=self.get_book_ids_to_use(),
|
||||||
book_ids=self.db.search('', return_matches=True, sort_results=False),
|
first_letter_sort = self.collapse_model == 'first letter')
|
||||||
first_letter_sort = self.collapse_model == 'first letter')
|
except:
|
||||||
except:
|
import traceback
|
||||||
import traceback
|
traceback.print_exc()
|
||||||
traceback.print_exc()
|
|
||||||
data = self.db.new_api.get_categories(sort=sort, icon_map=self.category_icon_map,
|
|
||||||
first_letter_sort = self.collapse_model == 'first letter')
|
|
||||||
self.restriction_error.emit()
|
|
||||||
else:
|
|
||||||
data = self.db.new_api.get_categories(sort=sort, icon_map=self.category_icon_map,
|
data = self.db.new_api.get_categories(sort=sort, icon_map=self.category_icon_map,
|
||||||
first_letter_sort = self.collapse_model == 'first letter')
|
first_letter_sort = self.collapse_model == 'first letter')
|
||||||
|
self.restriction_error.emit()
|
||||||
|
|
||||||
# Reconstruct the user categories, putting them into metadata
|
# Reconstruct the user categories, putting them into metadata
|
||||||
self.db.field_metadata.remove_dynamic_categories()
|
self.db.field_metadata.remove_dynamic_categories()
|
||||||
@ -1042,21 +1043,14 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
item.tag.name = val
|
item.tag.name = val
|
||||||
self.search_item_renamed.emit() # Does a refresh
|
self.search_item_renamed.emit() # Does a refresh
|
||||||
else:
|
else:
|
||||||
if key == 'series':
|
restrict_to_book_ids=self.get_book_ids_to_use()
|
||||||
self.db.rename_series(item.tag.id, val)
|
self.db.new_api.rename_items(key, {item.tag.id: val},
|
||||||
elif key == 'publisher':
|
restrict_to_book_ids=restrict_to_book_ids)
|
||||||
self.db.rename_publisher(item.tag.id, val)
|
|
||||||
elif key == 'tags':
|
|
||||||
self.db.rename_tag(item.tag.id, val)
|
|
||||||
elif key == 'authors':
|
|
||||||
self.db.rename_author(item.tag.id, val)
|
|
||||||
elif self.db.field_metadata[key]['is_custom']:
|
|
||||||
self.db.rename_custom_item(item.tag.id, val,
|
|
||||||
label=self.db.field_metadata[key]['label'])
|
|
||||||
self.tag_item_renamed.emit()
|
self.tag_item_renamed.emit()
|
||||||
item.tag.name = val
|
item.tag.name = val
|
||||||
item.tag.state = TAG_SEARCH_STATES['clear']
|
item.tag.state = TAG_SEARCH_STATES['clear']
|
||||||
self.rename_item_in_all_user_categories(name, key, val)
|
if not restrict_to_book_ids:
|
||||||
|
self.rename_item_in_all_user_categories(name, key, val)
|
||||||
self.refresh_required.emit()
|
self.refresh_required.emit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -223,14 +223,18 @@ class TagBrowserMixin(object): # {{{
|
|||||||
if (category in ['tags', 'series', 'publisher'] or
|
if (category in ['tags', 'series', 'publisher'] or
|
||||||
db.new_api.field_metadata.is_custom_field(category)):
|
db.new_api.field_metadata.is_custom_field(category)):
|
||||||
m = self.tags_view.model()
|
m = self.tags_view.model()
|
||||||
for item in to_delete:
|
restrict_to_book_ids = m.get_book_ids_to_use()
|
||||||
m.delete_item_from_all_user_categories(orig_name[item], category)
|
if not restrict_to_book_ids:
|
||||||
for old_id in to_rename:
|
for item in to_delete:
|
||||||
|
m.delete_item_from_all_user_categories(orig_name[item], category)
|
||||||
|
for old_id in to_rename and not restrict_to_book_ids:
|
||||||
m.rename_item_in_all_user_categories(orig_name[old_id],
|
m.rename_item_in_all_user_categories(orig_name[old_id],
|
||||||
category, unicode(to_rename[old_id]))
|
category, unicode(to_rename[old_id]))
|
||||||
|
|
||||||
db.new_api.remove_items(category, to_delete)
|
db.new_api.remove_items(category, to_delete,
|
||||||
db.new_api.rename_items(category, to_rename, change_index=False)
|
restrict_to_book_ids=restrict_to_book_ids)
|
||||||
|
db.new_api.rename_items(category, to_rename, change_index=False,
|
||||||
|
restrict_to_book_ids=restrict_to_book_ids)
|
||||||
|
|
||||||
# Clean up the library view
|
# Clean up the library view
|
||||||
self.do_tag_item_renamed()
|
self.do_tag_item_renamed()
|
||||||
@ -260,8 +264,10 @@ class TagBrowserMixin(object): # {{{
|
|||||||
delete_func = partial(db.delete_custom_item_using_id, label=cc_label)
|
delete_func = partial(db.delete_custom_item_using_id, label=cc_label)
|
||||||
m = self.tags_view.model()
|
m = self.tags_view.model()
|
||||||
if delete_func:
|
if delete_func:
|
||||||
delete_func(item_id)
|
restrict_to_book_ids=m.get_book_ids_to_use()
|
||||||
m.delete_item_from_all_user_categories(orig_name, category)
|
delete_func(item_id, restrict_to_book_ids=restrict_to_book_ids)
|
||||||
|
if not restrict_to_book_ids:
|
||||||
|
m.delete_item_from_all_user_categories(orig_name, category)
|
||||||
|
|
||||||
# Clean up the library view
|
# Clean up the library view
|
||||||
self.do_tag_item_renamed()
|
self.do_tag_item_renamed()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user