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):
null_val = NULL_VALUES.get(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
def __getattribute__(self, field):
@ -129,6 +127,8 @@ class Metadata(object):
field, val = self._clean_identifier(field, val)
_data['identifiers'].update({field: val})
elif field == 'identifiers':
if not val:
val = copy.copy(NULL_VALUES.get('identifiers', None))
self.set_identifiers(val)
elif field in STANDARD_METADATA_FIELDS:
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.utils.search_query_parser import saved_searches
from calibre.utils.icu import sort_key
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def __init__(self, window, initial_search=None):
QDialog.__init__(self, window)
def __init__(self, parent, initial_search=None):
QDialog.__init__(self, parent)
Ui_SavedSearchEditor.__init__(self)
self.setupUi(self)
@ -22,12 +23,13 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'),
self.current_index_changed)
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.searches = {}
self.searches_to_delete = []
for name in saved_searches().names():
self.searches[name] = saved_searches().lookup(name)
self.search_names = set([icu_lower(n) for n in saved_searches().names()])
self.populate_search_list()
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()
if search_name == '':
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:
self.searches[search_name] = ''
self.populate_search_list()
@ -57,10 +64,25 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
+'</p>', 'saved_search_editor_delete', self):
return
del self.searches[self.current_search_name]
self.searches_to_delete.append(self.current_search_name)
self.current_search_name = None
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):
self.search_name_box.setCurrentIndex(self.search_name_box.findText(name))
@ -78,7 +100,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def accept(self):
if self.current_search_name:
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)
for name in self.searches:
saved_searches().add(name, self.searches[name])

View File

@ -134,6 +134,20 @@
</property>
</widget>
</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>
</item>
<item row="1" column="0">

View File

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

View File

@ -533,7 +533,9 @@ class TagsView(QTreeView): # {{{
self.setModel(self._model)
except:
# 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.setModel(None)
# }}}
@ -678,7 +680,8 @@ class TagTreeItem(object): # {{{
break
elif self.tag.state == TAG_SEARCH_STATES['mark_plusplus'] or\
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
else:
break
@ -1258,19 +1261,22 @@ class TagsModel(QAbstractItemModel): # {{{
if t.type != TagTreeItem.CATEGORY])
if (comp,tag.category) in child_map:
node_parent = child_map[(comp,tag.category)]
node_parent.tag.is_hierarchical = True
node_parent.tag.is_hierarchical = key != 'search'
else:
if i < len(components)-1:
t = copy.copy(tag)
t.original_name = '.'.join(components[:i+1])
# This 'manufactured' intermediate node can
# be searched, but cannot be edited.
t.is_editable = False
if key != 'search':
# This 'manufactured' intermediate node can
# be searched, but cannot be edited.
t.is_editable = False
else:
t.is_searchable = t.is_editable = False
else:
t = tag
if not in_uc:
t.original_name = t.name
t.is_hierarchical = True
t.is_hierarchical = key != 'search'
t.name = comp
self.beginInsertRows(category_index, 999999, 1)
node_parent = TagTreeItem(parent=node_parent, data=t,

View File

@ -56,7 +56,7 @@ class Tag(object):
self.is_hierarchical = False
self.is_editable = is_editable
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.sort = sort
if self.avg_rating > 0:
@ -1691,10 +1691,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return books_to_refresh
def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
set_authors=True, commit=True, force_cover=False,
force_tags=False):
set_authors=True, commit=True, force_changes=False):
'''
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)):
# Handle code passing in a OPF object instead of a Metadata object
@ -1708,12 +1717,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc()
else:
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
if set_title and not mi.is_null('title'):
if set_title and mi.title:
self._set_title(id, mi.title)
path_changed = True
if set_authors and not mi.is_null('authors'):
if set_authors:
if not mi.authors:
mi.authors = [_('Unknown')]
authors = []
for a in mi.authors:
authors += string_to_authors(a)
@ -1722,17 +1737,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if path_changed:
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,
commit=False)
if not mi.is_null('publisher'):
if should_replace_field('publisher'):
doit(self.set_publisher, id, mi.publisher, notify=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)
if not mi.is_null('series'):
if should_replace_field('series'):
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:
doit(self.set_cover, id, mi.cover_data[1], commit=False)
elif mi.cover is not None:
@ -1741,36 +1759,45 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
raw = f.read()
if raw:
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)
if not mi.is_null('comments'):
if should_replace_field('comments'):
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,
commit=False)
if not mi.is_null('pubdate'):
if should_replace_field('pubdate'):
doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False)
if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False,
commit=False)
# identifiers will always be replaced if force_changes is True
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)
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
self.set_identifiers(id, identifiers, notify=False, commit=False)
user_mi = mi.get_all_user_metadata(make_copy=False)
for key in user_mi.iterkeys():
if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
doit(self.set_custom, id, val=mi.get(key), commit=False,
extra=mi.get_extra(key), label=user_mi[key]['label'])
val = mi.get(key, None)
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:
self.conn.commit()
self.notify('metadata', [id])