Make item delete and rename take VLs into consideration

This commit is contained in:
Charles Haley 2015-02-27 16:11:56 +01:00
parent fd5dad9bce
commit 5557660bbd
5 changed files with 145 additions and 48 deletions

View File

@ -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})

View File

@ -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)))
# }}} # }}}

View File

@ -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:

View File

@ -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

View File

@ -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()