diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py index 0edf08c405..c106582bfa 100644 --- a/src/calibre/ebooks/metadata/book/__init__.py +++ b/src/calibre/ebooks/metadata/book/__init__.py @@ -89,11 +89,18 @@ CALIBRE_METADATA_FIELDS = frozenset([ ) CALIBRE_RESERVED_LABELS = frozenset([ - 'search', # reserved for saved searches - 'date', - 'all', - 'ondevice', - 'inlibrary', + 'all', # search term + 'author_sort', # can appear in device collection customization + 'date', # search term + 'formats', # search term + 'inlibrary', # search term + 'news', # search term + 'ondevice', # search term + 'search', # search term + 'format', # The next four are here for backwards compatibility + 'tag', # with searching. The terms can be used without the + 'author', # trailing 's'. + 'comment', # Sigh ... ] ) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index bc0367b766..18af6d8560 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -631,7 +631,10 @@ class BooksModel(QAbstractTableModel): # {{{ if section >= len(self.column_map): # same problem as in data, the column_map can be wrong return None if role == Qt.ToolTipRole: - return QVariant(_('The lookup/search name is "{0}"').format(self.column_map[section])) + ht = self.column_map[section] + if ht == 'timestamp': # change help text because users know this field as 'date' + ht = 'date' + return QVariant(_('The lookup/search name is "{0}"').format(ht)) if role == Qt.DisplayRole: return QVariant(self.headers[self.column_map[section]]) return NONE @@ -730,11 +733,13 @@ class BooksModel(QAbstractTableModel): # {{{ class OnDeviceSearch(SearchQueryParser): # {{{ USABLE_LOCATIONS = [ - 'collections', - 'title', - 'author', - 'format', 'all', + 'author', + 'authors', + 'collections', + 'format', + 'formats', + 'title', ] diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 6d1bf5ab28..8bbdc69c62 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -14,8 +14,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \ QAbstractItemModel, QVariant, QModelIndex from calibre.gui2 import config, NONE from calibre.utils.config import prefs -from calibre.utils.search_query_parser import saved_searches -from calibre.library.database2 import Tag +from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS class TagsView(QTreeView): # {{{ @@ -204,17 +203,22 @@ class TagsModel(QAbstractItemModel): # {{{ QAbstractItemModel.__init__(self, parent) # must do this here because 'QPixmap: Must construct a QApplication - # before a QPaintDevice' - self.category_icon_map = {'authors': QIcon(I('user_profile.svg')), - 'series': QIcon(I('series.svg')), - 'formats':QIcon(I('book.svg')), - 'publishers': QIcon(I('publisher.png')), - 'ratings':QIcon(I('star.png')), - 'news':QIcon(I('news.svg')), - 'tags':QIcon(I('tags.svg')), - '*custom':QIcon(I('column.svg')), - '*user':QIcon(I('drawer.svg')), - 'search':QIcon(I('search.svg'))} + # before a QPaintDevice'. The ':' in front avoids polluting either the + # user-defined categories (':' at end) or columns namespaces (no ':'). + self.category_icon_map = { + 'authors' : QIcon(I('user_profile.svg')), + 'series' : QIcon(I('series.svg')), + 'formats' : QIcon(I('book.svg')), + 'publisher' : QIcon(I('publisher.png')), + 'rating' : QIcon(I('star.png')), + 'news' : QIcon(I('news.svg')), + 'tags' : QIcon(I('tags.svg')), + ':custom' : QIcon(I('column.svg')), + ':user' : QIcon(I('drawer.svg')), + 'search' : QIcon(I('search.svg'))} + for k in self.category_icon_map.keys(): + if not k.startswith(':') and k not in RESERVED_METADATA_FIELDS: + raise ValueError('Tag category [%s] is not a reserved word.' %(k)) self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))] self.db = db self.search_restriction = '' @@ -381,9 +385,9 @@ class TagsModel(QAbstractItemModel): # {{{ def tokens(self): ans = [] - tags_seen = [] + tags_seen = set() for i, key in enumerate(self.row_map): - if key.endswith('*'): # User category, so skip it. The tag will be marked in its real category + if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category continue category_item = self.root_item.children[i] for tag_item in category_item.children: @@ -394,10 +398,10 @@ class TagsModel(QAbstractItemModel): # {{{ if tag.name[0] == u'\u2605': # char is a star. Assume rating ans.append('%s%s:%s'%(prefix, category, len(tag.name))) else: - if category == 'tag': + if category == 'tags': if tag.name in tags_seen: continue - tags_seen.append(tag.name) + tags_seen.add(tag.name) ans.append('%s%s:"=%s"'%(prefix, category, tag.name)) return ans diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 8278386b8e..84124b6ce9 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -37,6 +37,7 @@ from calibre.utils.ordered_dict import OrderedDict from calibre.utils.config import prefs from calibre.utils.search_query_parser import saved_searches from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format +from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS if iswindows: import calibre.utils.winshell as winshell @@ -137,10 +138,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ('formats', {'table':None, 'column':None, 'type':None, 'is_multiple':False, 'kind':'standard', 'name':_('Formats')}), - ('publishers',{'table':'publishers', 'column':'name', + ('publisher', {'table':'publishers', 'column':'name', 'type':'text', 'is_multiple':False, 'kind':'standard', 'name':_('Publishers')}), - ('ratings', {'table':'ratings', 'column':'rating', + ('rating', {'table':'ratings', 'column':'rating', 'type':'rating', 'is_multiple':False, 'kind':'standard', 'name':_('Ratings')}), ('news', {'table':'news', 'column':'name', @@ -152,6 +153,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): ] self.tag_browser_categories = OrderedDict() for k,v in tag_browser_categories_items: + if k not in RESERVED_METADATA_FIELDS: + raise ValueError('Tag category [%s] is not a reserved word.' %(k)) self.tag_browser_categories[k] = v self.connect() @@ -694,25 +697,25 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if category in icon_map: icon = icon_map[category] elif self.tag_browser_categories[category]['kind'] == 'custom': - icon = icon_map['*custom'] - icon_map[category] = icon_map['*custom'] + icon = icon_map[':custom'] + icon_map[category] = icon tooltip = self.custom_column_label_map[category]['name'] datatype = self.tag_browser_categories[category]['type'] if datatype == 'rating': - item_zero_func = (lambda x: len(formatter(r[1])) > 0) + item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0) formatter = (lambda x:u'\u2605'*int(round(x/2.))) elif category == 'authors': - item_zero_func = (lambda x: x[2] > 0) + item_not_zero_func = (lambda x: x[2] > 0) # Clean up the authors strings to human-readable form formatter = (lambda x: x.replace('|', ',')) else: - item_zero_func = (lambda x: x[2] > 0) + item_not_zero_func = (lambda x: x[2] > 0) formatter = (lambda x:x) categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], icon=icon, tooltip = tooltip) - for r in data if item_zero_func(r)] + for r in data if item_not_zero_func(r)] # We delayed computing the standard formats category because it does not # use a view, but is computed dynamically @@ -767,14 +770,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): items.append(taglist[label][name]) # else: do nothing, to not include nodes w zero counts if len(items): - cat_name = user_cat+'*' # add the * to avoid name collision + cat_name = user_cat+':' # add the ':' to avoid name collision self.tag_browser_categories[cat_name] = { 'table':None, 'column':None, 'type':None, 'is_multiple':False, 'kind':'user', 'name':user_cat} # Not a problem if we accumulate entries in the icon map if icon_map is not None: - icon_map[cat_name] = icon_map['*user'] + icon_map[cat_name] = icon_map[':user'] if sort_on_count: categories[cat_name] = \ sorted(items, cmp=(lambda x, y: cmp(y.count, x.count))) diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index 11991727b7..5fe0a242f8 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -22,7 +22,7 @@ from calibre.utils.pyparsing import Keyword, Group, Forward, CharsNotIn, Suppres OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch, ParseException from calibre.constants import preferred_encoding from calibre.utils.config import prefs - +from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS ''' This class manages access to the preference holding the saved search queries. @@ -87,21 +87,25 @@ class SearchQueryParser(object): ''' DEFAULT_LOCATIONS = [ - 'tag', - 'title', - 'author', + 'all', + 'author', # compatibility + 'authors', + 'comment', # compatibility + 'comments', + 'cover', + 'date', + 'format', # compatibility + 'formats', + 'isbn', + 'ondevice', + 'pubdate', 'publisher', + 'search', 'series', 'rating', - 'cover', - 'comments', - 'format', - 'isbn', - 'search', - 'date', - 'pubdate', - 'ondevice', - 'all', + 'tag', # compatibility + 'tags', + 'title', ] @staticmethod @@ -118,6 +122,9 @@ class SearchQueryParser(object): return failed def __init__(self, locations=None, test=False): + for k in self.DEFAULT_LOCATIONS: + if k not in RESERVED_METADATA_FIELDS: + raise ValueError('Search location [%s] is not a reserved word.' %(k)) if locations is None: locations = self.DEFAULT_LOCATIONS self._tests_failed = False