Make searches in the tag browser a possible hierarchical field.

This commit is contained in:
Kovid Goyal 2011-03-13 10:38:09 -06:00
commit d4db55d524
6 changed files with 104 additions and 34 deletions

View File

@ -92,8 +92,6 @@ class Metadata(object):
def is_null(self, field): def is_null(self, field):
null_val = NULL_VALUES.get(field, None) null_val = NULL_VALUES.get(field, None)
val = getattr(self, field, None) val = getattr(self, field, None)
if val is False or val in (0, 0.0):
return True
return not val or val == null_val return not val or val == null_val
def __getattribute__(self, field): def __getattribute__(self, field):
@ -129,6 +127,8 @@ class Metadata(object):
field, val = self._clean_identifier(field, val) field, val = self._clean_identifier(field, val)
_data['identifiers'].update({field: val}) _data['identifiers'].update({field: val})
elif field == 'identifiers': elif field == 'identifiers':
if not val:
val = copy.copy(NULL_VALUES.get('identifiers', None))
self.set_identifiers(val) self.set_identifiers(val)
elif field in STANDARD_METADATA_FIELDS: elif field in STANDARD_METADATA_FIELDS:
if val is None: if val is None:

View File

@ -9,12 +9,13 @@ from PyQt4.QtGui import QDialog
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def __init__(self, window, initial_search=None): def __init__(self, parent, initial_search=None):
QDialog.__init__(self, window) QDialog.__init__(self, parent)
Ui_SavedSearchEditor.__init__(self) Ui_SavedSearchEditor.__init__(self)
self.setupUi(self) self.setupUi(self)
@ -22,12 +23,13 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'), self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'),
self.current_index_changed) self.current_index_changed)
self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search) self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search)
self.rename_button.clicked.connect(self.rename_search)
self.current_search_name = None self.current_search_name = None
self.searches = {} self.searches = {}
self.searches_to_delete = []
for name in saved_searches().names(): for name in saved_searches().names():
self.searches[name] = saved_searches().lookup(name) self.searches[name] = saved_searches().lookup(name)
self.search_names = set([icu_lower(n) for n in saved_searches().names()])
self.populate_search_list() self.populate_search_list()
if initial_search is not None and initial_search in self.searches: if initial_search is not None and initial_search in self.searches:
@ -42,6 +44,11 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
search_name = unicode(self.input_box.text()).strip() search_name = unicode(self.input_box.text()).strip()
if search_name == '': if search_name == '':
return False return False
if icu_lower(search_name) in self.search_names:
error_dialog(self, _('Saved search already exists'),
_('The saved search %s already exists, perhaps with '
'different case')%search_name).exec_()
return False
if search_name not in self.searches: if search_name not in self.searches:
self.searches[search_name] = '' self.searches[search_name] = ''
self.populate_search_list() self.populate_search_list()
@ -57,10 +64,25 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
+'</p>', 'saved_search_editor_delete', self): +'</p>', 'saved_search_editor_delete', self):
return return
del self.searches[self.current_search_name] del self.searches[self.current_search_name]
self.searches_to_delete.append(self.current_search_name)
self.current_search_name = None self.current_search_name = None
self.search_name_box.removeItem(self.search_name_box.currentIndex()) self.search_name_box.removeItem(self.search_name_box.currentIndex())
def rename_search(self):
new_search_name = unicode(self.input_box.text()).strip()
if new_search_name == '':
return False
if icu_lower(new_search_name) in self.search_names:
error_dialog(self, _('Saved search already exists'),
_('The saved search %s already exists, perhaps with '
'different case')%new_search_name).exec_()
return False
if self.current_search_name in self.searches:
self.searches[new_search_name] = self.searches[self.current_search_name]
del self.searches[self.current_search_name]
self.populate_search_list()
self.select_search(new_search_name)
return True
def select_search(self, name): def select_search(self, name):
self.search_name_box.setCurrentIndex(self.search_name_box.findText(name)) self.search_name_box.setCurrentIndex(self.search_name_box.findText(name))
@ -78,7 +100,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def accept(self): def accept(self):
if self.current_search_name: if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText()) self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
for name in self.searches_to_delete: for name in saved_searches().names():
saved_searches().delete(name) saved_searches().delete(name)
for name in self.searches: for name in self.searches:
saved_searches().add(name, self.searches[name]) saved_searches().add(name, self.searches[name])

View File

@ -134,6 +134,20 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="6">
<widget class="QToolButton" name="rename_button">
<property name="toolTip">
<string>Rename the current search to what is in the box</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/edit-undo.png</normaloff>:/images/edit-undo.png</iconset>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item row="1" column="0"> <item row="1" column="0">

View File

@ -67,6 +67,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if db.field_metadata[k]['is_category'] and if db.field_metadata[k]['is_category'] and
db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']]) db.field_metadata[k]['datatype'] in ['text', 'series', 'enumeration']])
choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers']) choices -= set(['authors', 'publisher', 'formats', 'news', 'identifiers'])
choices |= set(['search'])
self.opt_categories_using_hierarchy.update_items_cache(choices) self.opt_categories_using_hierarchy.update_items_cache(choices)
r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList, r('categories_using_hierarchy', db.prefs, setting=CommaSeparatedList,
choices=sorted(list(choices), key=sort_key)) choices=sorted(list(choices), key=sort_key))

View File

@ -533,7 +533,9 @@ class TagsView(QTreeView): # {{{
self.setModel(self._model) self.setModel(self._model)
except: except:
# The DB must be gone. Set the model to None and hope that someone # The DB must be gone. Set the model to None and hope that someone
# will call set_database later. I don't know if this in fact works # will call set_database later. I don't know if this in fact works.
# But perhaps a Bad Thing Happened, so print the exception
traceback.print_exc()
self._model = None self._model = None
self.setModel(None) self.setModel(None)
# }}} # }}}
@ -678,7 +680,8 @@ class TagTreeItem(object): # {{{
break break
elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\ elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\
self.tag.state == TAG_SEARCH_STATES['mark_minusminus']: self.tag.state == TAG_SEARCH_STATES['mark_minusminus']:
if self.tag.is_hierarchical and len(self.children): if self.tag.is_searchable and self.tag.is_hierarchical \
and len(self.children):
break break
else: else:
break break
@ -1258,19 +1261,22 @@ class TagsModel(QAbstractItemModel): # {{{
if t.type != TagTreeItem.CATEGORY]) if t.type != TagTreeItem.CATEGORY])
if (comp,tag.category) in child_map: if (comp,tag.category) in child_map:
node_parent = child_map[(comp,tag.category)] node_parent = child_map[(comp,tag.category)]
node_parent.tag.is_hierarchical = True node_parent.tag.is_hierarchical = key != 'search'
else: else:
if i < len(components)-1: if i < len(components)-1:
t = copy.copy(tag) t = copy.copy(tag)
t.original_name = '.'.join(components[:i+1]) t.original_name = '.'.join(components[:i+1])
# This 'manufactured' intermediate node can if key != 'search':
# be searched, but cannot be edited. # This 'manufactured' intermediate node can
t.is_editable = False # be searched, but cannot be edited.
t.is_editable = False
else:
t.is_searchable = t.is_editable = False
else: else:
t = tag t = tag
if not in_uc: if not in_uc:
t.original_name = t.name t.original_name = t.name
t.is_hierarchical = True t.is_hierarchical = key != 'search'
t.name = comp t.name = comp
self.beginInsertRows(category_index, 999999, 1) self.beginInsertRows(category_index, 999999, 1)
node_parent = TagTreeItem(parent=node_parent, data=t, node_parent = TagTreeItem(parent=node_parent, data=t,

View File

@ -56,7 +56,7 @@ class Tag(object):
self.is_hierarchical = False self.is_hierarchical = False
self.is_editable = is_editable self.is_editable = is_editable
self.is_searchable = is_searchable self.is_searchable = is_searchable
self.id_set = id_set self.id_set = id_set if id_set is not None else set([])
self.avg_rating = avg/2.0 if avg is not None else 0 self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort self.sort = sort
if self.avg_rating > 0: if self.avg_rating > 0:
@ -1691,10 +1691,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return books_to_refresh return books_to_refresh
def set_metadata(self, id, mi, ignore_errors=False, set_title=True, def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
set_authors=True, commit=True, force_cover=False, set_authors=True, commit=True, force_changes=False):
force_tags=False):
''' '''
Set metadata for the book `id` from the `Metadata` object `mi` Set metadata for the book `id` from the `Metadata` object `mi`
Setting force_changes=True will force set_metadata to update fields even
if mi contains empty values. In this case, 'None' is distinguished from
'empty'. If mi.XXX is None, the XXX is not replaced, otherwise it is.
The tags, identifiers, and cover attributes are special cases. Tags and
identifiers cannot be set to None so then will always be replaced if
force_changes is true. You must ensure that mi contains the values you
want the book to have. Covers are always changed if a new cover is
provided, but are never deleted. Also note that force_changes has no
effect on setting title or authors.
''' '''
if callable(getattr(mi, 'to_book_metadata', None)): if callable(getattr(mi, 'to_book_metadata', None)):
# Handle code passing in a OPF object instead of a Metadata object # Handle code passing in a OPF object instead of a Metadata object
@ -1708,12 +1717,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc() traceback.print_exc()
else: else:
raise raise
# force_changes has no role to play in setting title or author
def should_replace_field(attr):
return (force_changes and (mi.get(attr, None) is not None)) or \
not mi.is_null(attr)
path_changed = False path_changed = False
if set_title and not mi.is_null('title'): if set_title and mi.title:
self._set_title(id, mi.title) self._set_title(id, mi.title)
path_changed = True path_changed = True
if set_authors and not mi.is_null('authors'): if set_authors:
if not mi.authors:
mi.authors = [_('Unknown')]
authors = [] authors = []
for a in mi.authors: for a in mi.authors:
authors += string_to_authors(a) authors += string_to_authors(a)
@ -1722,17 +1737,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if path_changed: if path_changed:
self.set_path(id, index_is_id=True) self.set_path(id, index_is_id=True)
if not mi.is_null('author_sort'): if should_replace_field('author_sort'):
doit(self.set_author_sort, id, mi.author_sort, notify=False, doit(self.set_author_sort, id, mi.author_sort, notify=False,
commit=False) commit=False)
if not mi.is_null('publisher'): if should_replace_field('publisher'):
doit(self.set_publisher, id, mi.publisher, notify=False, doit(self.set_publisher, id, mi.publisher, notify=False,
commit=False) commit=False)
if not mi.is_null('rating'):
# Setting rating to zero is acceptable.
if mi.rating is not None:
doit(self.set_rating, id, mi.rating, notify=False, commit=False) doit(self.set_rating, id, mi.rating, notify=False, commit=False)
if not mi.is_null('series'): if should_replace_field('series'):
doit(self.set_series, id, mi.series, notify=False, commit=False) doit(self.set_series, id, mi.series, notify=False, commit=False)
# force_changes has no effect on cover manipulation
if mi.cover_data[1] is not None: if mi.cover_data[1] is not None:
doit(self.set_cover, id, mi.cover_data[1], commit=False) doit(self.set_cover, id, mi.cover_data[1], commit=False)
elif mi.cover is not None: elif mi.cover is not None:
@ -1741,36 +1759,45 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
raw = f.read() raw = f.read()
if raw: if raw:
doit(self.set_cover, id, raw, commit=False) doit(self.set_cover, id, raw, commit=False)
elif force_cover:
doit(self.remove_cover, id, notify=False, commit=False)
if force_tags or not mi.is_null('tags'): # if force_changes is true, tags are always replaced because the
# attribute cannot be set to None.
if should_replace_field('tags'):
doit(self.set_tags, id, mi.tags, notify=False, commit=False) doit(self.set_tags, id, mi.tags, notify=False, commit=False)
if not mi.is_null('comments'):
if should_replace_field('comments'):
doit(self.set_comment, id, mi.comments, notify=False, commit=False) doit(self.set_comment, id, mi.comments, notify=False, commit=False)
if not mi.is_null('series_index'):
# Setting series_index to zero is acceptable
if mi.series_index is not None:
doit(self.set_series_index, id, mi.series_index, notify=False, doit(self.set_series_index, id, mi.series_index, notify=False,
commit=False) commit=False)
if not mi.is_null('pubdate'): if should_replace_field('pubdate'):
doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False) doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False)
if getattr(mi, 'timestamp', None) is not None: if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False, doit(self.set_timestamp, id, mi.timestamp, notify=False,
commit=False) commit=False)
# identifiers will always be replaced if force_changes is True
mi_idents = mi.get_identifiers() mi_idents = mi.get_identifiers()
if mi_idents: if force_changes:
self.set_identifiers(id, mi_idents, notify=False, commit=False)
elif mi_idents:
identifiers = self.get_identifiers(id, index_is_id=True) identifiers = self.get_identifiers(id, index_is_id=True)
for key, val in mi_idents.iteritems(): for key, val in mi_idents.iteritems():
if val and val.strip(): if val and val.strip(): # Don't delete an existing identifier
identifiers[icu_lower(key)] = val identifiers[icu_lower(key)] = val
self.set_identifiers(id, identifiers, notify=False, commit=False) self.set_identifiers(id, identifiers, notify=False, commit=False)
user_mi = mi.get_all_user_metadata(make_copy=False) user_mi = mi.get_all_user_metadata(make_copy=False)
for key in user_mi.iterkeys(): for key in user_mi.iterkeys():
if key in self.field_metadata and \ if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']: user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
doit(self.set_custom, id, val=mi.get(key), commit=False, val = mi.get(key, None)
extra=mi.get_extra(key), label=user_mi[key]['label']) if force_changes or val is not None:
doit(self.set_custom, id, val=val, extra=mi.get_extra(key),
label=user_mi[key]['label'], commit=False)
if commit: if commit:
self.conn.commit() self.conn.commit()
self.notify('metadata', [id]) self.notify('metadata', [id])