mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Renaming of many-(one,many) items
This commit is contained in:
parent
22f2aca3eb
commit
0c6d820f2b
@ -1213,6 +1213,33 @@ class Cache(object):
|
||||
else:
|
||||
table.remove_books(book_ids, self.backend)
|
||||
|
||||
@read_api
|
||||
def author_sort_strings_for_books(self, book_ids):
|
||||
val_map = {}
|
||||
for book_id in book_ids:
|
||||
authors = self._field_ids_for('authors', book_id)
|
||||
adata = self._author_data(authors)
|
||||
val_map[book_id] = tuple(adata[aid]['sort'] for aid in authors)
|
||||
return val_map
|
||||
|
||||
@write_api
|
||||
def rename_items(self, field, item_id_to_new_name_map):
|
||||
try:
|
||||
func = self.fields[field].table.rename_item
|
||||
except AttributeError:
|
||||
raise ValueError('Cannot rename items for one-one fields: %s' % field)
|
||||
affected_books = set()
|
||||
for item_id, new_name in item_id_to_new_name_map.iteritems():
|
||||
affected_books.update(func(item_id, new_name, self.backend))
|
||||
if affected_books:
|
||||
if field == 'authors':
|
||||
self._set_field('author_sort', # also marks as dirty
|
||||
{k:' & '.join(v) for k, v in self._author_sort_strings_for_books(affected_books).iteritems()})
|
||||
self._update_path(affected_books, mark_as_dirtied=False)
|
||||
else:
|
||||
self._mark_as_dirty(affected_books)
|
||||
return affected_books
|
||||
|
||||
@write_api
|
||||
def remove_items(self, field, item_ids):
|
||||
''' Delete all items in the specified field with the specified ids. Returns the set of affected book ids. '''
|
||||
|
@ -290,10 +290,7 @@ class LibraryDatabase(object):
|
||||
|
||||
def authors_sort_strings(self, index, index_is_id=False):
|
||||
book_id = index if index_is_id else self.id(index)
|
||||
with self.new_api.read_lock:
|
||||
authors = self.new_api._field_ids_for('authors', book_id)
|
||||
adata = self.new_api._author_data(authors)
|
||||
return [adata[aid]['sort'] for aid in authors]
|
||||
return list(self.author_sort_strings_for_books.canonical_author_sort_for_books((book_id,))[book_id])
|
||||
|
||||
def author_sort_from_book(self, index, index_is_id=False):
|
||||
return ' & '.join(self.authors_sort_strings(index, index_is_id=index_is_id))
|
||||
|
@ -222,6 +222,29 @@ class ManyToOneTable(Table):
|
||||
db.conn.executemany('DELETE FROM {0} WHERE id=?'.format(self.metadata['table']), item_ids)
|
||||
return affected_books
|
||||
|
||||
def rename_item(self, item_id, new_name, db):
|
||||
rmap = {icu_lower(v):k for k, v in self.id_map.iteritems()}
|
||||
existing_item = rmap.get(icu_lower(new_name), None)
|
||||
table, col, lcol = self.metadata['table'], self.metadata['column'], self.metadata['link_column']
|
||||
affected_books = self.col_book_map.get(item_id, set())
|
||||
if existing_item is None or existing_item == item_id:
|
||||
# A simple rename will do the trick
|
||||
self.id_map[item_id] = new_name
|
||||
db.conn.execute('UPDATE {0} SET {1}=? WHERE id=?'.format(table, col), (new_name, item_id))
|
||||
else:
|
||||
# We have to replace
|
||||
self.id_map.pop(item_id, None)
|
||||
books = self.col_book_map.pop(item_id, set())
|
||||
for book_id in books:
|
||||
self.book_col_map[book_id] = existing_item
|
||||
self.col_book_map[existing_item].update(books)
|
||||
# For custom series this means that the series index can
|
||||
# potentially have duplicates/be incorrect, but there is no way to
|
||||
# handle that in this context.
|
||||
db.conn.execute('UPDATE {0} SET {1}=? WHERE {1}=?; DELETE FROM {2} WHERE id=?'.format(
|
||||
self.link_table, lcol, table), (existing_item, item_id, item_id))
|
||||
return affected_books
|
||||
|
||||
class ManyToManyTable(ManyToOneTable):
|
||||
|
||||
'''
|
||||
@ -283,6 +306,32 @@ class ManyToManyTable(ManyToOneTable):
|
||||
db.conn.executemany('DELETE FROM {0} WHERE id=?'.format(self.metadata['table']), item_ids)
|
||||
return affected_books
|
||||
|
||||
def rename_item(self, item_id, new_name, db):
|
||||
rmap = {icu_lower(v):k for k, v in self.id_map.iteritems()}
|
||||
existing_item = rmap.get(icu_lower(new_name), None)
|
||||
table, col, lcol = self.metadata['table'], self.metadata['column'], self.metadata['link_column']
|
||||
affected_books = self.col_book_map.get(item_id, set())
|
||||
if existing_item is None or existing_item == item_id:
|
||||
# A simple rename will do the trick
|
||||
self.id_map[item_id] = new_name
|
||||
db.conn.execute('UPDATE {0} SET {1}=? WHERE id=?'.format(table, col), (new_name, item_id))
|
||||
else:
|
||||
# We have to replace
|
||||
self.id_map.pop(item_id, None)
|
||||
books = self.col_book_map.pop(item_id, set())
|
||||
# Replacing item_id with existing_item could cause the same id to
|
||||
# appear twice in the book list. Handle that by removing existing
|
||||
# item from the book list before replacing.
|
||||
for book_id in books:
|
||||
self.book_col_map[book_id] = tuple((existing_item if x == item_id else x) for x in self.book_col_map.get(book_id, ()) if x != existing_item)
|
||||
self.col_book_map[existing_item].update(books)
|
||||
db.conn.executemany('DELETE FROM {0} WHERE book=? AND {1}=?'.format(self.link_table, lcol), [
|
||||
(book_id, existing_item) for book_id in books])
|
||||
db.conn.execute('UPDATE {0} SET {1}=? WHERE {1}=?; DELETE FROM {2} WHERE id=?'.format(
|
||||
self.link_table, lcol, table), (existing_item, item_id, item_id))
|
||||
return affected_books
|
||||
|
||||
|
||||
class AuthorsTable(ManyToManyTable):
|
||||
|
||||
def read_id_maps(self, db):
|
||||
@ -314,6 +363,17 @@ class AuthorsTable(ManyToManyTable):
|
||||
self.asort_map.pop(item_id, None)
|
||||
return clean
|
||||
|
||||
def rename_item(self, item_id, new_name, db):
|
||||
ret = ManyToManyTable.rename_item(self, item_id, new_name, db)
|
||||
if item_id not in self.id_map:
|
||||
self.alink_map.pop(item_id, None)
|
||||
self.asort_map.pop(item_id, None)
|
||||
else:
|
||||
# Was a simple rename, update the author sort value
|
||||
self.set_sort_names({item_id:author_to_author_sort(new_name)}, db)
|
||||
|
||||
return ret
|
||||
|
||||
def remove_items(self, item_ids, db):
|
||||
raise ValueError('Direct removal of authors is not allowed')
|
||||
|
||||
@ -377,6 +437,9 @@ class FormatsTable(ManyToManyTable):
|
||||
def remove_items(self, item_ids, db):
|
||||
raise NotImplementedError('Cannot delete a format directly')
|
||||
|
||||
def rename_item(self, item_id, new_name, db):
|
||||
raise NotImplementedError('Cannot rename formats')
|
||||
|
||||
def update_fmt(self, book_id, fmt, fname, size, db):
|
||||
fmts = list(self.book_col_map.get(book_id, []))
|
||||
try:
|
||||
@ -430,6 +493,9 @@ class IdentifiersTable(ManyToManyTable):
|
||||
def remove_items(self, item_ids, db):
|
||||
raise NotImplementedError('Direct deletion of identifiers is not implemented')
|
||||
|
||||
def rename_item(self, item_id, new_name, db):
|
||||
raise NotImplementedError('Cannot rename identifiers')
|
||||
|
||||
def all_identifier_types(self):
|
||||
return frozenset(k for k, v in self.col_book_map.iteritems() if v)
|
||||
|
||||
|
@ -474,3 +474,72 @@ class WritingTest(BaseTest):
|
||||
for bid in c.all_book_ids():
|
||||
self.assertIn(c.field_for('#series', bid), (None, 'My Series One'))
|
||||
# }}}
|
||||
|
||||
def test_rename_items(self): # {{{
|
||||
' Test renaming of many-(many,one) items '
|
||||
cl = self.cloned_library
|
||||
cache = self.init_cache(cl)
|
||||
# Check that renaming authors updates author sort and path
|
||||
a = {v:k for k, v in cache.get_id_map('authors').iteritems()}['Unknown']
|
||||
self.assertEqual(cache.rename_items('authors', {a:'New Author'}), {3})
|
||||
a = {v:k for k, v in cache.get_id_map('authors').iteritems()}['Author One']
|
||||
self.assertEqual(cache.rename_items('authors', {a:'Author Two'}), {1, 2})
|
||||
for c in (cache, self.init_cache(cl)):
|
||||
self.assertEqual(c.all_field_names('authors'), {'New Author', 'Author Two'})
|
||||
self.assertEqual(c.field_for('author_sort', 3), 'Author, New')
|
||||
self.assertIn('New Author/', c.field_for('path', 3))
|
||||
self.assertEqual(c.field_for('authors', 1), ('Author Two',))
|
||||
self.assertEqual(c.field_for('author_sort', 1), 'Two, Author')
|
||||
|
||||
t = {v:k for k, v in cache.get_id_map('tags').iteritems()}['Tag One']
|
||||
# Test case change
|
||||
self.assertEqual(cache.rename_items('tags', {t:'tag one'}), {1, 2})
|
||||
for c in (cache, self.init_cache(cl)):
|
||||
self.assertEqual(c.all_field_names('tags'), {'tag one', 'Tag Two', 'News'})
|
||||
self.assertEqual(set(c.field_for('tags', 1)), {'tag one', 'News'})
|
||||
self.assertEqual(set(c.field_for('tags', 2)), {'tag one', 'Tag Two'})
|
||||
# Test new name
|
||||
self.assertEqual(cache.rename_items('tags', {t:'t1'}), {1,2})
|
||||
for c in (cache, self.init_cache(cl)):
|
||||
self.assertEqual(c.all_field_names('tags'), {'t1', 'Tag Two', 'News'})
|
||||
self.assertEqual(set(c.field_for('tags', 1)), {'t1', 'News'})
|
||||
self.assertEqual(set(c.field_for('tags', 2)), {'t1', 'Tag Two'})
|
||||
# Test rename to existing
|
||||
self.assertEqual(cache.rename_items('tags', {t:'Tag Two'}), {1,2})
|
||||
for c in (cache, self.init_cache(cl)):
|
||||
self.assertEqual(c.all_field_names('tags'), {'Tag Two', 'News'})
|
||||
self.assertEqual(set(c.field_for('tags', 1)), {'Tag Two', 'News'})
|
||||
self.assertEqual(set(c.field_for('tags', 2)), {'Tag Two'})
|
||||
# Test on a custom column
|
||||
t = {v:k for k, v in cache.get_id_map('#tags').iteritems()}['My Tag One']
|
||||
self.assertEqual(cache.rename_items('#tags', {t:'My Tag Two'}), {2})
|
||||
for c in (cache, self.init_cache(cl)):
|
||||
self.assertEqual(c.all_field_names('#tags'), {'My Tag Two'})
|
||||
self.assertEqual(set(c.field_for('#tags', 2)), {'My Tag Two'})
|
||||
|
||||
# Test a Many-one field
|
||||
s = {v:k for k, v in cache.get_id_map('series').iteritems()}['A Series One']
|
||||
# Test case change
|
||||
self.assertEqual(cache.rename_items('series', {s:'a series one'}), {1, 2})
|
||||
for c in (cache, self.init_cache(cl)):
|
||||
self.assertEqual(c.all_field_names('series'), {'a series one'})
|
||||
self.assertEqual(c.field_for('series', 1), 'a series one')
|
||||
self.assertEqual(c.field_for('series_index', 1), 2.0)
|
||||
|
||||
# Test new name
|
||||
self.assertEqual(cache.rename_items('series', {s:'series'}), {1, 2})
|
||||
for c in (cache, self.init_cache(cl)):
|
||||
self.assertEqual(c.all_field_names('series'), {'series'})
|
||||
self.assertEqual(c.field_for('series', 1), 'series')
|
||||
self.assertEqual(c.field_for('series', 2), 'series')
|
||||
self.assertEqual(c.field_for('series_index', 1), 2.0)
|
||||
|
||||
s = {v:k for k, v in cache.get_id_map('#series').iteritems()}['My Series One']
|
||||
# Test custom column with rename to existing
|
||||
self.assertEqual(cache.rename_items('#series', {s:'My Series Two'}), {2})
|
||||
for c in (cache, self.init_cache(cl)):
|
||||
self.assertEqual(c.all_field_names('#series'), {'My Series Two'})
|
||||
self.assertEqual(c.field_for('#series', 2), 'My Series Two')
|
||||
self.assertEqual(c.field_for('#series_index', 1), 3.0)
|
||||
self.assertEqual(c.field_for('#series_index', 2), 1.0)
|
||||
# }}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user