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
@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.
: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]
try:
func = f.table.rename_item
except AttributeError:
raise ValueError('Cannot rename items for one-one fields: %s' % field)
affected_books = set()
moved_books = set()
id_map = {}
try:
sv = f.metadata['is_multiple']['ui_to_list']
except (TypeError, KeyError, AttributeError):
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():
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)
@ -1606,10 +1642,11 @@ class Cache(object):
return affected_books, id_map
@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. '''
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 hasattr(field, 'index_field'):
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'):
def renamer(field):
def func(self, old_id, new_name):
id_map = self.new_api.rename_items(field, {old_id:new_name})[1]
def func(self, old_id, new_name, restrict_to_book_ids=None):
id_map = self.new_api.rename_items(field, {old_id:new_name},
restrict_to_book_ids=restrict_to_book_ids)[1]
if field == 'authors':
return id_map[old_id]
return func
@ -877,8 +878,8 @@ for field in ('author', 'tag', 'series'):
for field in ('publisher', 'series', 'tag'):
def getter(field):
fname = 'tags' if field == 'tag' else field
def func(self, item_id):
self.new_api.remove_items(fname, (item_id,))
def func(self, item_id, restrict_to_book_ids=None):
self.new_api.remove_items(fname, (item_id,), restrict_to_book_ids=restrict_to_book_ids)
return func
setattr(LibraryDatabase, 'delete_%s_using_id' % field, MT(getter(field)))
# }}}

View File

@ -265,8 +265,37 @@ class ManyToOneTable(Table):
[(x,) for x in 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()
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:
val = self.id_map.pop(item_id, null)
if val is null:
@ -373,8 +402,38 @@ class ManyToManyTable(ManyToOneTable):
[(x,) for x in 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()
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:
val = self.id_map.pop(item_id, null)
if val is null:

View File

@ -855,6 +855,11 @@ class TagsModel(QAbstractItemModel): # {{{
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):
'''
Called by __init__. Do not directly call this method.
@ -863,11 +868,10 @@ class TagsModel(QAbstractItemModel): # {{{
self.categories = {}
# Get the categories
if self.db.data.get_base_restriction() or self.db.data.get_search_restriction():
try:
data = self.db.new_api.get_categories(sort=sort,
icon_map=self.category_icon_map,
book_ids=self.db.search('', return_matches=True, sort_results=False),
book_ids=self.get_book_ids_to_use(),
first_letter_sort = self.collapse_model == 'first letter')
except:
import traceback
@ -875,9 +879,6 @@ class TagsModel(QAbstractItemModel): # {{{
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,
first_letter_sort = self.collapse_model == 'first letter')
# Reconstruct the user categories, putting them into metadata
self.db.field_metadata.remove_dynamic_categories()
@ -1042,20 +1043,13 @@ class TagsModel(QAbstractItemModel): # {{{
item.tag.name = val
self.search_item_renamed.emit() # Does a refresh
else:
if key == 'series':
self.db.rename_series(item.tag.id, val)
elif key == 'publisher':
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'])
restrict_to_book_ids=self.get_book_ids_to_use()
self.db.new_api.rename_items(key, {item.tag.id: val},
restrict_to_book_ids=restrict_to_book_ids)
self.tag_item_renamed.emit()
item.tag.name = val
item.tag.state = TAG_SEARCH_STATES['clear']
if not restrict_to_book_ids:
self.rename_item_in_all_user_categories(name, key, val)
self.refresh_required.emit()
return True

View File

@ -223,14 +223,18 @@ class TagBrowserMixin(object): # {{{
if (category in ['tags', 'series', 'publisher'] or
db.new_api.field_metadata.is_custom_field(category)):
m = self.tags_view.model()
restrict_to_book_ids = m.get_book_ids_to_use()
if not restrict_to_book_ids:
for item in to_delete:
m.delete_item_from_all_user_categories(orig_name[item], category)
for old_id in to_rename:
for old_id in to_rename and not restrict_to_book_ids:
m.rename_item_in_all_user_categories(orig_name[old_id],
category, unicode(to_rename[old_id]))
db.new_api.remove_items(category, to_delete)
db.new_api.rename_items(category, to_rename, change_index=False)
db.new_api.remove_items(category, to_delete,
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
self.do_tag_item_renamed()
@ -260,7 +264,9 @@ class TagBrowserMixin(object): # {{{
delete_func = partial(db.delete_custom_item_using_id, label=cc_label)
m = self.tags_view.model()
if delete_func:
delete_func(item_id)
restrict_to_book_ids=m.get_book_ids_to_use()
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