From 5557660bbd02f4d342de6c3fddd5ab35b6255eb7 Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Fri, 27 Feb 2015 16:11:56 +0100 Subject: [PATCH] Make item delete and rename take VLs into consideration --- src/calibre/db/cache.py | 55 +++++++++++++++++++---- src/calibre/db/legacy.py | 9 ++-- src/calibre/db/tables.py | 63 ++++++++++++++++++++++++++- src/calibre/gui2/tag_browser/model.py | 46 +++++++++---------- src/calibre/gui2/tag_browser/ui.py | 20 ++++++--- 5 files changed, 145 insertions(+), 48 deletions(-) diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index d665534715..ebd8f77307 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -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}) diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index 84c2e75bb6..ab0e900347 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -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))) # }}} diff --git a/src/calibre/db/tables.py b/src/calibre/db/tables.py index 750714ef94..328f515f1d 100644 --- a/src/calibre/db/tables.py +++ b/src/calibre/db/tables.py @@ -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: diff --git a/src/calibre/gui2/tag_browser/model.py b/src/calibre/gui2/tag_browser/model.py index 57f9aca620..746c91bb56 100644 --- a/src/calibre/gui2/tag_browser/model.py +++ b/src/calibre/gui2/tag_browser/model.py @@ -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,21 +868,17 @@ 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), - first_letter_sort = self.collapse_model == 'first letter') - except: - import traceback - 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: + try: + data = self.db.new_api.get_categories(sort=sort, + icon_map=self.category_icon_map, + book_ids=self.get_book_ids_to_use(), + first_letter_sort = self.collapse_model == 'first letter') + except: + import traceback + 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') + first_letter_sort = self.collapse_model == 'first letter') + self.restriction_error.emit() # Reconstruct the user categories, putting them into metadata self.db.field_metadata.remove_dynamic_categories() @@ -1042,21 +1043,14 @@ 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'] - 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() return True diff --git a/src/calibre/gui2/tag_browser/ui.py b/src/calibre/gui2/tag_browser/ui.py index c46809db52..b2a0006955 100644 --- a/src/calibre/gui2/tag_browser/ui.py +++ b/src/calibre/gui2/tag_browser/ui.py @@ -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() - for item in to_delete: - m.delete_item_from_all_user_categories(orig_name[item], category) - for old_id in to_rename: + 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 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,8 +264,10 @@ 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) - m.delete_item_from_all_user_categories(orig_name, category) + 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 self.do_tag_item_renamed()