From e4ad4ceb71fc6a178e0192b718ef089ea2ad506d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 7 Jun 2010 19:00:33 +0100 Subject: [PATCH] Permit renaming on the tags pane to combine items (rename to same as existing item) --- src/calibre/gui2/tag_view.py | 23 +-- src/calibre/library/custom_columns.py | 33 +++-- src/calibre/library/database2.py | 202 +++++++++++++++++++------- 3 files changed, 185 insertions(+), 73 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index b1aecd9ba3..7fa374baca 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -138,7 +138,8 @@ class TagsView(QTreeView): # {{{ # the possibility of renaming that item if tag_name and \ (key in ['authors', 'tags', 'series', 'publisher', 'search'] or \ - self.db.field_metadata[key]['is_custom']): + self.db.field_metadata[key]['is_custom'] and \ + self.db.field_metadata[key]['datatype'] != 'rating'): self.context_menu.addAction(_('Rename') + " '" + tag_name + "'", partial(self.context_menu_handler, action='edit_item', category=tag_item, index=index)) @@ -359,12 +360,8 @@ class TagsModel(QAbstractItemModel): # {{{ data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map) tb_categories = self.db.field_metadata - self.category_items = {} for category in tb_categories: if category in data: # They should always be there, but ... - # make a map of sets of names per category for duplicate - # checking when editing - self.category_items[category] = set([tag.name for tag in data[category]]) self.row_map.append(category) self.categories.append(tb_categories[category]['name']) return data @@ -412,15 +409,14 @@ class TagsModel(QAbstractItemModel): # {{{ return False item = index.internalPointer() key = item.parent.category_key - # make certain we know about the category + # make certain we know about the item's category if key not in self.db.field_metadata: return - if val in self.category_items[key]: - error_dialog(self.tags_view, 'Duplicate item', - _('The name %s is already used.')%val).exec_() - return False - oldval = item.tag.name if key == 'search': + if val in saved_searches.names(): + error_dialog(self.tags_view, _('Duplicate search name'), + _('The saved search name %s is already used.')%val).exec_() + return False saved_searches.rename(unicode(item.data(role).toString()), val) self.tags_view.search_item_renamed.emit() else: @@ -437,10 +433,7 @@ class TagsModel(QAbstractItemModel): # {{{ label=self.db.field_metadata[key]['label']) self.tags_view.tag_item_renamed.emit() item.tag.name = val - self.dataChanged.emit(index, index) - # replace the old value in the duplicate detection map with the new one - self.category_items[key].discard(oldval) - self.category_items[key].add(val) + self.refresh() return True def headerData(self, *args): diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 4d2c8970b6..d9e16a5e32 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -183,15 +183,30 @@ class CustomColumns(object): ans = self.conn.get('SELECT id, value FROM %s'%table) return ans - def rename_custom_item(self, id, new_name, label=None, num=None): - if id: - if label is not None: - data = self.custom_column_label_map[label] - if num is not None: - data = self.custom_column_num_map[num] - table,lt = self.custom_table_names(data['num']) - self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, id)) - self.conn.commit() + def rename_custom_item(self, old_id, new_name, label=None, num=None): + if label is not None: + data = self.custom_column_label_map[label] + if num is not None: + data = self.custom_column_num_map[num] + table,lt = self.custom_table_names(data['num']) + # check if item exists + new_id = self.conn.get( + 'SELECT id FROM %s WHERE value=?'%table, (new_name,), all=False) + if new_id is None: + self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, old_id)) + else: + # New id exists. If the column is_multiple, then process like + # tags, otherwise process like publishers (see database2) + if data['is_multiple']: + books = self.conn.get('''SELECT book from %s + WHERE value=?'''%lt, (old_id,)) + for (book_id,) in books: + self.conn.execute('''DELETE FROM %s + WHERE book=? and value=?'''%lt, (book_id, new_id)) + self.conn.execute('''UPDATE %s SET value=? + WHERE value=?'''%lt, (new_id, old_id,)) + self.conn.execute('DELETE FROM %s WHERE id=?'%table, (old_id,)) + self.conn.commit() def delete_custom_item_using_id(self, id, label=None, num=None): if id: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index f1eeb23643..f4791c929c 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -999,16 +999,37 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return [] return result - def rename_tag(self, id, new_name): - if id: - self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new_name, id)) - self.conn.commit() + def rename_tag(self, old_id, new_name): + new_id = self.conn.get( + '''SELECT id from tags + WHERE name=?''', (new_name,), all=False) + if new_id is None: + # easy case. Simply rename the tag + self.conn.execute('''UPDATE tags SET name=? + WHERE id=?''', (new_name, old_id)) + else: + # It is possible that by renaming a tag, the tag will appear + # twice on a book. This will throw an integrity error, aborting + # all the changes. To get around this, we first delete any links + # to the new_id from books referencing the old_id, so that + # renaming old_id to new_id will be unique on the book + books = self.conn.get('''SELECT book from books_tags_link + WHERE tag=?''', (old_id,)) + for (book_id,) in books: + self.conn.execute('''DELETE FROM books_tags_link + WHERE book=? and tag=?''', (book_id, new_id)) + + # Change the link table to point at the new tag + self.conn.execute('''UPDATE books_tags_link SET tag=? + WHERE tag=?''',(new_id, old_id,)) + # Get rid of the no-longer used publisher + self.conn.execute('DELETE FROM tags WHERE id=?', (old_id,)) + self.conn.commit() def delete_tag_using_id(self, id): - if id: - self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,)) - self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) - self.conn.commit() + self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,)) + self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) + self.conn.commit() def get_series_with_ids(self): result = self.conn.get('SELECT id,name FROM series') @@ -1016,19 +1037,42 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return [] return result - def rename_series(self, id, new_name): - if id: - self.conn.execute('UPDATE series SET name=? WHERE id=?', (new_name, id)) - self.conn.commit() + def rename_series(self, old_id, new_name): + new_id = self.conn.get( + '''SELECT id from series + WHERE name=?''', (new_name,), all=False) + if new_id is None: + self.conn.execute('UPDATE series SET name=? WHERE id=?', + (new_name, old_id)) + else: + # New series exists. Must update the link, then assign a + # new series index to each of the books. + + # Get the list of books where we must update the series index + books = self.conn.get('''SELECT book from books_series_link + WHERE series=?''', (old_id,)) + # Get the next series index + index = self.get_next_series_num_for(new_name) + # Now update the link table + self.conn.execute('''UPDATE books_series_link + SET series=? + WHERE series=?''',(new_id, old_id,)) + # Now set the indices + for (book_id,) in books: + self.conn.execute('''UPDATE books + SET series_index=? + WHERE id=?''',(index, book_id,)) + index = index + 1 + self.conn.commit() + def delete_series_using_id(self, id): - if id: - books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,)) - self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,)) - self.conn.execute('DELETE FROM series WHERE id=?', (id,)) - self.conn.commit() - for (book_id,) in books: - self.conn.execute('UPDATE books SET series_index=1.0 WHERE id=?', (book_id,)) + books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,)) + self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,)) + self.conn.execute('DELETE FROM series WHERE id=?', (id,)) + self.conn.commit() + for (book_id,) in books: + self.conn.execute('UPDATE books SET series_index=1.0 WHERE id=?', (book_id,)) def get_publishers_with_ids(self): result = self.conn.get('SELECT id,name FROM publishers') @@ -1036,43 +1080,103 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return [] return result - def rename_publisher(self, id, new_name): - if id: - self.conn.execute('UPDATE publishers SET name=? WHERE id=?', (new_name, id)) - self.conn.commit() + def rename_publisher(self, old_id, new_name): + new_id = self.conn.get( + '''SELECT id from publishers + WHERE name=?''', (new_name,), all=False) + if new_id is None: + # New name doesn't exist. Simply change the old name + self.conn.execute('UPDATE publishers SET name=? WHERE id=?', \ + (new_name, old_id)) + else: + # Change the link table to point at the new one + self.conn.execute('''UPDATE books_publishers_link + SET publisher=? + WHERE publisher=?''',(new_id, old_id,)) + # Get rid of the no-longer used publisher + self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,)) + self.conn.commit() - def delete_publisher_using_id(self, id): - if id: - self.conn.execute('DELETE FROM books_publishers_link WHERE publisher=?', (id,)) - self.conn.execute('DELETE FROM publishers WHERE id=?', (id,)) - self.conn.commit() + def delete_publisher_using_id(self, old_id): + self.conn.execute('''DELETE FROM books_publishers_link + WHERE publisher=?''', (old_id,)) + self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,)) + self.conn.commit() # There is no editor for author, so we do not need get_authors_with_ids or # delete_author_using_id. - def rename_author(self, id, new_name): - if id: - # Make sure that any commas in new_name are changed to '|'! - new_name = new_name.replace(',', '|') - self.conn.execute('UPDATE authors SET name=? WHERE id=?', (new_name, id)) - self.conn.commit() - # now must fix up the books - books = self.conn.get('SELECT book from books_authors_link WHERE author=?', (id,)) + + def rename_author(self, old_id, new_name): + # Make sure that any commas in new_name are changed to '|'! + new_name = new_name.replace(',', '|') + + # Get the list of books we must fix up, one way or the other + books = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,)) + + # check if the new author already exists + new_id = self.conn.get('SELECT id from authors WHERE name=?', + (new_name,), all=False) + if new_id is None: + # No name clash. Go ahead and update the author's name + self.conn.execute('UPDATE authors SET name=? WHERE id=?', + (new_name, old_id)) + else: + # Author exists. To fix this, we must replace all the authors + # instead of replacing the one. Reason: db integrity checks can stop + # the rename process, which would leave everything half-done. We + # can't do it the same way as tags (delete and add) because author + # order is important. for (book_id,) in books: - # First, must refresh the cache to see the new authors - self.data.refresh_ids(self, [book_id]) - # now fix the filesystem paths - self.set_path(book_id, index_is_id=True) - # Next fix the author sort. Reset it to the default + # Get the existing list of authors authors = self.conn.get(''' - SELECT authors.name - FROM authors, books_authors_link as bl - WHERE bl.book = ? and bl.author = authors.id - ''' , (book_id,)) - # unpack the double-list structure + SELECT author from books_authors_link + WHERE book=? + ORDER BY id''',(book_id,)) + + # unpack the double-list structure, replacing the old author + # with the new one while we are at it for i,aut in enumerate(authors): - authors[i] = aut[0] - ss = authors_to_sort_string(authors) - self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id)) + authors[i] = aut[0] if aut[0] != old_id else new_id + + # Delete the existing authors list + self.conn.execute('''DELETE FROM books_authors_link + WHERE book=?''',(book_id,)) + # Change the authors to the new list + for aid in authors: + try: + self.conn.execute(''' + INSERT INTO books_authors_link(book, author) + VALUES (?,?)''', (book_id, aid)) + except IntegrityError: + # Sometimes books specify the same author twice in their + # metadata. Ignore it. + pass + # Now delete the old author from the DB + self.conn.execute('DELETE FROM authors WHERE id=?', (old_id,)) + self.conn.commit() + # the authors are now changed, either by changing the author's name + # or replacing the author in the list. Now must fix up the books. + for (book_id,) in books: + # First, must refresh the cache to see the new authors + self.data.refresh_ids(self, [book_id]) + # now fix the filesystem paths + self.set_path(book_id, index_is_id=True) + # Next fix the author sort. Reset it to the default + authors = self.conn.get(''' + SELECT authors.name + FROM authors, books_authors_link as bl + WHERE bl.book = ? and bl.author = authors.id + ''' , (book_id,)) + # unpack the double-list structure + for i,aut in enumerate(authors): + authors[i] = aut[0] + ss = authors_to_sort_string(authors) + self.conn.execute('''UPDATE books + SET author_sort=? + WHERE id=?''', (ss, old_id)) + self.conn.commit() + # the caller will do a general refresh, so we don't need to + # do one here # end convenience methods