diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py index 131692a2c2..64743e914b 100644 --- a/src/calibre/gui2/add.py +++ b/src/calibre/gui2/add.py @@ -222,6 +222,8 @@ class DBAdder(Thread): class Adder(QObject): + ADD_TIMEOUT = 600 # seconds + def __init__(self, parent, db, callback, spare_server=None): QObject.__init__(self, parent) self.pd = ProgressDialog(_('Adding...'), parent=parent) @@ -328,7 +330,7 @@ class Adder(QObject): except Empty: pass - if (time.time() - self.last_added_at) > 300: + if (time.time() - self.last_added_at) > self.ADD_TIMEOUT: self.timer.stop() self.pd.hide() self.db_adder.end = True diff --git a/src/calibre/gui2/dialogs/saved_search_editor.py b/src/calibre/gui2/dialogs/saved_search_editor.py index a9382201b9..6a8b790625 100644 --- a/src/calibre/gui2/dialogs/saved_search_editor.py +++ b/src/calibre/gui2/dialogs/saved_search_editor.py @@ -3,14 +3,12 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -from PyQt4.QtCore import SIGNAL, Qt -from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem +from PyQt4.QtCore import SIGNAL +from PyQt4.QtGui import QDialog from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor -from calibre.utils.config import prefs from calibre.utils.search_query_parser import saved_searches from calibre.gui2.dialogs.confirm_delete import confirm -from calibre.constants import islinux class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index c2cc1d7116..1ec80f4b4a 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -1,17 +1,17 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' + +from functools import partial from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtGui import QDialog, QListWidgetItem from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor from calibre.gui2 import question_dialog, error_dialog +from calibre.ebooks.metadata import title_sort class TagListEditor(QDialog, Ui_TagListEditor): - def tag_cmp(self, x, y): - return cmp(x.lower(), y.lower()) - - def __init__(self, window, db, tag_to_match): + def __init__(self, window, db, tag_to_match, category): QDialog.__init__(self, window) Ui_TagListEditor.__init__(self) self.setupUi(self) @@ -20,9 +20,28 @@ class TagListEditor(QDialog, Ui_TagListEditor): self.to_delete = [] self.db = db self.all_tags = {} - for k,v in db.get_tags_with_ids(): + self.category = category + if category == 'tags': + result = db.get_tags_with_ids() + compare = (lambda x,y:cmp(x.lower(), y.lower())) + elif category == 'series': + result = db.get_series_with_ids() + compare = (lambda x,y:cmp(title_sort(x).lower(), title_sort(y).lower())) + elif category == 'publisher': + result = db.get_publishers_with_ids() + compare = (lambda x,y:cmp(x.lower(), y.lower())) + else: # should be a custom field + self.cc_label = None + if category in db.field_metadata: + self.cc_label = db.field_metadata[category]['label'] + result = self.db.get_custom_items_with_ids(label=self.cc_label) + else: + result = [] + compare = (lambda x,y:cmp(x.lower(), y.lower())) + + for k,v in result: self.all_tags[v] = k - for tag in sorted(self.all_tags.keys(), cmp=self.tag_cmp): + for tag in sorted(self.all_tags.keys(), cmp=compare): item = QListWidgetItem(tag) item.setData(Qt.UserRole, self.all_tags[tag]) self.available_tags.addItem(item) @@ -37,13 +56,18 @@ class TagListEditor(QDialog, Ui_TagListEditor): self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing) def finish_editing(self, item): - if item.text() != self.item_before_editing.text(): - if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys(): - error_dialog(self, 'Tag already used', - 'The tag %s is already used.'%(item.text())).exec_() + if not item.text(): + error_dialog(self, _('Item is blank'), + _('An item cannot be set to nothing. Delete it instead.')).exec_() item.setText(self.item_before_editing.text()) return - id,ign = self.item_before_editing.data(Qt.UserRole).toInt() + if item.text() != self.item_before_editing.text(): + if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys(): + error_dialog(self, _('Item already used'), + _('The item %s is already used.')%(item.text())).exec_() + item.setText(self.item_before_editing.text()) + return + (id,ign) = self.item_before_editing.data(Qt.UserRole).toInt() self.to_rename[item.text()] = id def rename_tag(self): @@ -52,38 +76,53 @@ class TagListEditor(QDialog, Ui_TagListEditor): def _rename_tag(self, item): if item is None: - error_dialog(self, 'No tag selected', 'You must select one tag from the list of Available tags.').exec_() + error_dialog(self, _('No item selected'), + _('You must select one item from the list of Available items.')).exec_() return self.item_before_editing = item.clone() item.setFlags (item.flags() | Qt.ItemIsEditable); self.available_tags.editItem(item) def delete_tags(self, item=None): - confirms, deletes = [], [] - items = self.available_tags.selectedItems() if item is None else [item] - if not items: - error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec_() + deletes = self.available_tags.selectedItems() if item is None else [item] + if not deletes: + error_dialog(self, _('No items selected'), + _('You must select at least one items from the list.')).exec_() + return + ct = ', '.join([unicode(item.text()) for item in deletes]) + if not question_dialog(self, _('Are your sure?'), + '

'+_('Are you certain you want to delete the following items?')+'
'+ct): return - for item in items: - if self.db.is_tag_used(unicode(item.text())): - confirms.append(item) - else: - deletes.append(item) - if confirms: - ct = ', '.join([unicode(item.text()) for item in confirms]) - if question_dialog(self, _('Are your sure?'), - '

'+_('The following tags are used by one or more books. ' - 'Are you certain you want to delete them?')+'
'+ct): - deletes += confirms for item in deletes: - self.to_delete.append(item) + (id,ign) = item.data(Qt.UserRole).toInt() + self.to_delete.append(id) self.available_tags.takeItem(self.available_tags.row(item)) def accept(self): - for text in self.to_rename: - self.db.rename_tag(self.to_rename[text], unicode(text)) - for item in self.to_delete: - self.db.delete_tag(unicode(item.text())) - QDialog.accept(self) + rename_func = None + if self.category == 'tags': + rename_func = self.db.rename_tag + delete_func = self.db.delete_tag_using_id + elif self.category == 'series': + rename_func = self.db.rename_series + delete_func = self.db.delete_series_using_id + elif self.category == 'publisher': + rename_func = self.db.rename_publisher + delete_func = self.db.delete_publisher_using_id + else: + rename_func = partial(self.db.rename_custom_item, label=self.cc_label) + delete_func = partial(self.db.delete_custom_item_using_id, label=self.cc_label) + work_done = False + if rename_func: + for text in self.to_rename: + work_done = True + rename_func(id=self.to_rename[text], new_name=unicode(text)) + for item in self.to_delete: + work_done = True + delete_func(item) + if not work_done: + QDialog.reject(self) + else: + QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/tag_list_editor.ui b/src/calibre/gui2/dialogs/tag_list_editor.ui index 383dc875ac..4f57af745b 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.ui +++ b/src/calibre/gui2/dialogs/tag_list_editor.ui @@ -11,7 +11,7 @@ - Tag Editor + Category Editor @@ -25,7 +25,7 @@ - Tags in use + Items in use available_tags @@ -54,7 +54,7 @@ - Delete tag from database. This will unapply the tag from all books and then remove it from the database. + Delete item from database. This will unapply the item from all books and then remove it from the database. ... @@ -74,7 +74,7 @@ - Rename the tag everywhere it is used. + Rename the item in every book where it is used. ... diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 11db157ed4..80f6bfa264 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -17,15 +17,18 @@ from calibre.gui2 import config, NONE from calibre.utils.config import prefs from calibre.library.field_metadata import TagsIcons from calibre.utils.search_query_parser import saved_searches +from calibre.gui2 import error_dialog class TagsView(QTreeView): # {{{ - need_refresh = pyqtSignal() + refresh_required = pyqtSignal() restriction_set = pyqtSignal(object) tags_marked = pyqtSignal(object, object) user_category_edit = pyqtSignal(object) - tag_list_edit = pyqtSignal(object) + tag_list_edit = pyqtSignal(object, object) saved_search_edit = pyqtSignal(object) + tag_item_renamed = pyqtSignal() + search_item_renamed = pyqtSignal() def __init__(self, *args): QTreeView.__init__(self, *args) @@ -36,7 +39,8 @@ class TagsView(QTreeView): # {{{ def set_database(self, db, tag_match, popularity, restriction): self.hidden_categories = config['tag_browser_hidden_categories'] - self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories) + self._model = TagsModel(db, parent=self, + hidden_categories=self.hidden_categories) self.popularity = popularity self.restriction = restriction self.tag_match = tag_match @@ -48,12 +52,12 @@ class TagsView(QTreeView): # {{{ self.popularity.setChecked(config['sort_by_popularity']) self.popularity.stateChanged.connect(self.sort_changed) self.restriction.activated[str].connect(self.search_restriction_set) - self.need_refresh.connect(self.recount, type=Qt.QueuedConnection) + self.refresh_required.connect(self.recount, type=Qt.QueuedConnection) db.add_listener(self.database_changed) self.saved_searches_changed(recount=False) def database_changed(self, event, ids): - self.need_refresh.emit() + self.refresh_required.emit() @property def match_all(self): @@ -80,18 +84,26 @@ class TagsView(QTreeView): # {{{ if event.button() == Qt.LeftButton: QTreeView.mouseReleaseEvent(self, event) + def mouseDoubleClickEvent(self, event): + # swallow these to avoid toggling and editing at the same time + pass + def toggle(self, index): modifiers = int(QApplication.keyboardModifiers()) exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT) if self._model.toggle(index, exclusive): self.tags_marked.emit(self._model.tokens(), self.match_all) - def context_menu_handler(self, action=None, category=None): + def context_menu_handler(self, action=None, category=None, + key=None, index=None): if not action: return try: - if action == 'manage_tags': - self.tag_list_edit.emit(category) + if action == 'edit_item': + self.edit(index) + return + if action == 'open_editor': + self.tag_list_edit.emit(category, key) return if action == 'manage_categories': self.user_category_edit.emit(category) @@ -117,29 +129,51 @@ class TagsView(QTreeView): # {{{ item = index.internalPointer() tag_name = '' if item.type == TagTreeItem.TAG: + tag_item = item tag_name = item.tag.name item = item.parent if item.type == TagTreeItem.CATEGORY: category = unicode(item.name.toString()) - self.context_menu = QMenu(self) - self.context_menu.addAction(_('Hide %s') % category, - partial(self.context_menu_handler, action='hide', category=category)) + key = item.category_key + # Verify that we are working with a field that we know something about + if key not in self.db.field_metadata: + return True - if self.hidden_categories: + self.context_menu = QMenu(self) + # If the user right-clicked on an editable item, then offer + # 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.context_menu.addAction(_('Rename') + " '" + tag_name + "'", + partial(self.context_menu_handler, action='edit_item', + category=tag_item, index=index)) self.context_menu.addSeparator() + # Hide/Show/Restore categories + self.context_menu.addAction(_('Hide category %s') % category, + partial(self.context_menu_handler, action='hide', category=category)) + if self.hidden_categories: m = self.context_menu.addMenu(_('Show category')) - for col in self.hidden_categories: + for col in sorted(self.hidden_categories, cmp=lambda x,y: cmp(x.lower(), y.lower())): m.addAction(col, partial(self.context_menu_handler, action='show', category=col)) - self.context_menu.addSeparator() - self.context_menu.addAction(_('Restore defaults'), + self.context_menu.addAction(_('Show all categories'), partial(self.context_menu_handler, action='defaults')) + # Offer specific editors for tags/series/publishers/saved searches self.context_menu.addSeparator() - self.context_menu.addAction(_('Manage Tags'), - partial(self.context_menu_handler, action='manage_tags', - category=tag_name)) + if key in ['tags', 'publisher', 'series'] or \ + self.db.field_metadata[key]['is_custom']: + self.context_menu.addAction(_('Manage ') + category, + partial(self.context_menu_handler, action='open_editor', + category=tag_name, key=key)) + elif key == 'search': + self.context_menu.addAction(_('Manage Saved Searches'), + partial(self.context_menu_handler, action='manage_searches', + category=tag_name)) + # Always show the user categories editor + self.context_menu.addSeparator() if category in prefs['user_categories'].keys(): self.context_menu.addAction(_('Manage User Categories'), partial(self.context_menu_handler, action='manage_categories', @@ -149,10 +183,6 @@ class TagsView(QTreeView): # {{{ partial(self.context_menu_handler, action='manage_categories', category=None)) - self.context_menu.addAction(_('Manage Saved Searches'), - partial(self.context_menu_handler, action='manage_searches', - category=tag_name)) - self.context_menu.popup(self.mapToGlobal(point)) return True @@ -203,7 +233,8 @@ class TagTreeItem(object): # {{{ TAG = 1 ROOT = 2 - def __init__(self, data=None, category_icon=None, icon_map=None, parent=None, tooltip=None): + def __init__(self, data=None, category_icon=None, icon_map=None, + parent=None, tooltip=None, category_key=None): self.parent = parent self.children = [] if self.parent is not None: @@ -218,6 +249,7 @@ class TagTreeItem(object): # {{{ self.bold_font = QFont() self.bold_font.setBold(True) self.bold_font = QVariant(self.bold_font) + self.category_key = category_key elif self.type == self.TAG: icon_map[0] = data.icon self.tag, self.icon_state_map = data, list(map(QVariant, icon_map)) @@ -263,6 +295,8 @@ class TagTreeItem(object): # {{{ return QVariant('%s'%(self.tag.name)) else: return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) + if role == Qt.EditRole: + return QVariant(self.tag.name) if role == Qt.DecorationRole: return self.icon_state_map[self.tag.state] if role == Qt.ToolTipRole and self.tag.tooltip is not None: @@ -277,7 +311,7 @@ class TagTreeItem(object): # {{{ class TagsModel(QAbstractItemModel): # {{{ - def __init__(self, db, parent=None, hidden_categories=None): + def __init__(self, db, parent, hidden_categories=None): QAbstractItemModel.__init__(self, parent) # must do this here because 'QPixmap: Must construct a QApplication @@ -297,6 +331,7 @@ class TagsModel(QAbstractItemModel): # {{{ self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))] self.db = db + self.tags_view = parent self.hidden_categories = hidden_categories self.search_restriction = '' self.ignore_next_search = 0 @@ -324,7 +359,7 @@ class TagsModel(QAbstractItemModel): # {{{ c = TagTreeItem(parent=self.root_item, data=self.categories[i], category_icon=self.category_icon_map[r], - tooltip=tt) + tooltip=tt, category_key=r) for tag in data[r]: TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) @@ -342,8 +377,12 @@ 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']) @@ -382,11 +421,52 @@ class TagsModel(QAbstractItemModel): # {{{ item = index.internalPointer() return item.data(role) + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid(): + return NONE + val = unicode(value.toString()) + if not val: + error_dialog(self.tags_view, _('Item is blank'), + _('An item cannot be set to nothing. Delete it instead.')).exec_() + return False + item = index.internalPointer() + key = item.parent.category_key + # make certain we know about the 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': + saved_searches.rename(unicode(item.data(role).toString()), val) + self.tags_view.search_item_renamed.emit() + 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']) + 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) + return True + def headerData(self, *args): return NONE def flags(self, *args): - return Qt.ItemIsEnabled|Qt.ItemIsSelectable + return Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable def path_for_index(self, index): ans = [] diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 773f44acd2..7546e461d6 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -553,6 +553,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.tags_view.tag_list_edit.connect(self.do_tags_list_edit) self.tags_view.user_category_edit.connect(self.do_user_categories_edit) self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) + self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed) + self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help) self.search.search.connect(self.tags_view.model().reinit) for x in (self.location_view.count_changed, self.tags_view.recount, self.restriction_count_changed): @@ -660,13 +662,22 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.tags_view.set_new_model() self.tags_view.recount() - def do_tags_list_edit(self, tag): - d = TagListEditor(self, self.library_view.model().db, tag) + def do_tags_list_edit(self, tag, category): + d = TagListEditor(self, self.library_view.model().db, tag, category) d.exec_() if d.result() == d.Accepted: + # Clean up everything, as information could have changed for many books. + self.library_view.model().refresh() self.tags_view.set_new_model() self.tags_view.recount() - self.library_view.model().refresh() + self.saved_search.clear_to_help() + self.search.clear_to_help() + + def do_tag_item_renamed(self): + # Clean up library view and search + self.library_view.model().refresh() + self.saved_search.clear_to_help() + self.search.clear_to_help() def do_saved_search_edit(self, search): d = SavedSearchEditor(self, search) diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 83e6b029cb..4d2c8970b6 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -171,6 +171,40 @@ class CustomColumns(object): ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower())) return ans + # convenience methods for tag editing + def get_custom_items_with_ids(self, 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']) + if not data['normalized']: + return [] + 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 delete_custom_item_using_id(self, id, 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('DELETE FROM %s WHERE value=?'%lt, (id,)) + self.conn.execute('DELETE FROM %s WHERE id=?'%table, (id,)) + self.conn.commit() + # end convenience methods + def all_custom(self, label=None, num=None): if label is not None: data = self.custom_column_label_map[label] diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 0544293095..29e1901ce2 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -643,11 +643,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ''' Remove orphaned entries. ''' - st = 'DELETE FROM %(table)s WHERE (SELECT COUNT(id) FROM books_%(ltable)s_link WHERE %(ltable_col)s=%(table)s.id) < 1;' - self.conn.execute(st%dict(ltable='authors', table='authors', ltable_col='author')) - self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher')) - self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag')) - self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series')) + def doit(ltable, table, ltable_col): + st = ('DELETE FROM books_%s_link WHERE (SELECT COUNT(id) ' + 'FROM books WHERE id=book) < 1;')%ltable + self.conn.execute(st) + st = ('DELETE FROM %(table)s WHERE (SELECT COUNT(id) ' + 'FROM books_%(ltable)s_link WHERE ' + '%(ltable_col)s=%(table)s.id) < 1;') % dict( + ltable=ltable, table=table, ltable_col=ltable_col) + self.conn.execute(st) + + for ltable, table, ltable_col in [ + ('authors', 'authors', 'author'), + ('publishers', 'publishers', 'publisher'), + ('tags', 'tags', 'tag'), + ('series', 'series', 'series') + ]: + doit(ltable, table, ltable_col) + for id_, tag in self.conn.get('SELECT id, name FROM tags', all=True): if not tag.strip(): self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', @@ -730,9 +743,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], icon=icon, tooltip = tooltip) for r in data if item_not_zero_func(r)] - if category == 'series': - categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name), - title_sort(y.name))) + if category == 'series' and not sort_on_count: + categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(), + title_sort(y.name).lower())) # We delayed computing the standard formats category because it does not # use a view, but is computed dynamically @@ -985,19 +998,91 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if notify: self.notify('metadata', [id]) - # Convenience method for tags_list_editor + # Convenience methods for tags_list_editor + # Note: we generally do not need to refresh_ids because library_view will + # refresh everything. def get_tags_with_ids(self): - result = self.conn.get('SELECT * FROM tags') + result = self.conn.get('SELECT id,name FROM tags') if not result: - return {} - r = [] - for k,v in result: - r.append((k,v)) - return r + return [] + return result - def rename_tag(self, id, new): - self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new, id)) - self.conn.commit() + 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 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() + + def get_series_with_ids(self): + result = self.conn.get('SELECT id,name FROM series') + if not result: + 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 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,)) + + def get_publishers_with_ids(self): + result = self.conn.get('SELECT id,name FROM publishers') + if not result: + 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 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() + + # 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,)) + 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, id)) + + # end convenience methods def get_tags(self, id): result = self.conn.get( @@ -1083,7 +1168,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) self.conn.commit() - def set_series(self, id, series, notify=True): self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,)) self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1') @@ -1603,6 +1687,7 @@ books_series_link feeds def check_integrity(self, callback): callback(0., _('Checking SQL integrity...')) + self.clean() user_version = self.user_version sql = '\n'.join(self.conn.dump()) self.conn.close() diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 07c90119bd..243e3646da 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -195,11 +195,11 @@ class FieldMetadata(dict): 'is_category':False}), ('ondevice', {'table':None, 'column':None, - 'datatype':'bool', + 'datatype':'text', 'is_multiple':None, 'kind':'field', 'name':None, - 'search_terms':[], + 'search_terms':['ondevice'], 'is_custom':False, 'is_category':False}), ('path', {'table':None, diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst index 0d79c77e55..98e31b2252 100644 --- a/src/calibre/manual/gui.rst +++ b/src/calibre/manual/gui.rst @@ -241,9 +241,9 @@ Now, you can access your saved search in the Tag Browser under "Searches". A sin .. _configuration: -Configuration +Preferences --------------- -The configuration dialog allows you to set some global defaults used by all of |app|. To access it, click the |cbi|. +The Preferences dialog allows you to set some global defaults used by all of |app|. To access it, click the |cbi|. .. |cbi| image:: images/configuration.png diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index 509adb49d4..d6bf932b76 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -52,6 +52,12 @@ class SavedSearchQueries(object): self.queries.pop(self.force_unicode(name), False) prefs[self.opt_name] = self.queries + def rename(self, old_name, new_name): + self.queries[self.force_unicode(new_name)] = \ + self.queries.get(self.force_unicode(old_name), None) + self.queries.pop(self.force_unicode(old_name), False) + prefs[self.opt_name] = self.queries + def names(self): return sorted(self.queries.keys(), cmp=lambda x,y: cmp(x.lower(), y.lower())) diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index af0c8da6b4..2a003c14f8 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal ' from lxml import html, etree from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \ - STRONG, BR, H1, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \ + STRONG, BR, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \ TABLE, TD, TR from calibre import preferred_encoding, strftime, isbytestring