Rationalize use of metadata field labels across tag browser and search

This commit is contained in:
Kovid Goyal 2010-05-25 09:52:39 -06:00
commit 3f49d4c483
5 changed files with 76 additions and 50 deletions

View File

@ -89,11 +89,18 @@ CALIBRE_METADATA_FIELDS = frozenset([
) )
CALIBRE_RESERVED_LABELS = frozenset([ CALIBRE_RESERVED_LABELS = frozenset([
'search', # reserved for saved searches 'all', # search term
'date', 'author_sort', # can appear in device collection customization
'all', 'date', # search term
'ondevice', 'formats', # search term
'inlibrary', '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 ...
] ]
) )

View File

@ -631,7 +631,10 @@ class BooksModel(QAbstractTableModel): # {{{
if section >= len(self.column_map): # same problem as in data, the column_map can be wrong if section >= len(self.column_map): # same problem as in data, the column_map can be wrong
return None return None
if role == Qt.ToolTipRole: 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: if role == Qt.DisplayRole:
return QVariant(self.headers[self.column_map[section]]) return QVariant(self.headers[self.column_map[section]])
return NONE return NONE
@ -730,11 +733,13 @@ class BooksModel(QAbstractTableModel): # {{{
class OnDeviceSearch(SearchQueryParser): # {{{ class OnDeviceSearch(SearchQueryParser): # {{{
USABLE_LOCATIONS = [ USABLE_LOCATIONS = [
'collections',
'title',
'author',
'format',
'all', 'all',
'author',
'authors',
'collections',
'format',
'formats',
'title',
] ]

View File

@ -14,8 +14,7 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QAbstractItemModel, QVariant, QModelIndex QAbstractItemModel, QVariant, QModelIndex
from calibre.gui2 import config, NONE from calibre.gui2 import config, NONE
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS
from calibre.library.database2 import Tag
class TagsView(QTreeView): # {{{ class TagsView(QTreeView): # {{{
@ -204,17 +203,22 @@ class TagsModel(QAbstractItemModel): # {{{
QAbstractItemModel.__init__(self, parent) QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication # must do this here because 'QPixmap: Must construct a QApplication
# before a QPaintDevice' # before a QPaintDevice'. The ':' in front avoids polluting either the
self.category_icon_map = {'authors': QIcon(I('user_profile.svg')), # user-defined categories (':' at end) or columns namespaces (no ':').
'series': QIcon(I('series.svg')), self.category_icon_map = {
'formats':QIcon(I('book.svg')), 'authors' : QIcon(I('user_profile.svg')),
'publishers': QIcon(I('publisher.png')), 'series' : QIcon(I('series.svg')),
'ratings':QIcon(I('star.png')), 'formats' : QIcon(I('book.svg')),
'news':QIcon(I('news.svg')), 'publisher' : QIcon(I('publisher.png')),
'tags':QIcon(I('tags.svg')), 'rating' : QIcon(I('star.png')),
'*custom':QIcon(I('column.svg')), 'news' : QIcon(I('news.svg')),
'*user':QIcon(I('drawer.svg')), 'tags' : QIcon(I('tags.svg')),
'search':QIcon(I('search.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.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db self.db = db
self.search_restriction = '' self.search_restriction = ''
@ -381,9 +385,9 @@ class TagsModel(QAbstractItemModel): # {{{
def tokens(self): def tokens(self):
ans = [] ans = []
tags_seen = [] tags_seen = set()
for i, key in enumerate(self.row_map): 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 continue
category_item = self.root_item.children[i] category_item = self.root_item.children[i]
for tag_item in category_item.children: 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 if tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name))) ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
else: else:
if category == 'tag': if category == 'tags':
if tag.name in tags_seen: if tag.name in tags_seen:
continue continue
tags_seen.append(tag.name) tags_seen.add(tag.name)
ans.append('%s%s:"=%s"'%(prefix, category, tag.name)) ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
return ans return ans

View File

@ -37,6 +37,7 @@ from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.ebooks.metadata.book import RESERVED_METADATA_FIELDS
if iswindows: if iswindows:
import calibre.utils.winshell as winshell import calibre.utils.winshell as winshell
@ -137,10 +138,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
('formats', {'table':None, 'column':None, ('formats', {'table':None, 'column':None,
'type':None, 'is_multiple':False, 'type':None, 'is_multiple':False,
'kind':'standard', 'name':_('Formats')}), 'kind':'standard', 'name':_('Formats')}),
('publishers',{'table':'publishers', 'column':'name', ('publisher', {'table':'publishers', 'column':'name',
'type':'text', 'is_multiple':False, 'type':'text', 'is_multiple':False,
'kind':'standard', 'name':_('Publishers')}), 'kind':'standard', 'name':_('Publishers')}),
('ratings', {'table':'ratings', 'column':'rating', ('rating', {'table':'ratings', 'column':'rating',
'type':'rating', 'is_multiple':False, 'type':'rating', 'is_multiple':False,
'kind':'standard', 'name':_('Ratings')}), 'kind':'standard', 'name':_('Ratings')}),
('news', {'table':'news', 'column':'name', ('news', {'table':'news', 'column':'name',
@ -152,6 +153,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
] ]
self.tag_browser_categories = OrderedDict() self.tag_browser_categories = OrderedDict()
for k,v in tag_browser_categories_items: 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.tag_browser_categories[k] = v
self.connect() self.connect()
@ -694,25 +697,25 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if category in icon_map: if category in icon_map:
icon = icon_map[category] icon = icon_map[category]
elif self.tag_browser_categories[category]['kind'] == 'custom': elif self.tag_browser_categories[category]['kind'] == 'custom':
icon = icon_map['*custom'] icon = icon_map[':custom']
icon_map[category] = icon_map['*custom'] icon_map[category] = icon
tooltip = self.custom_column_label_map[category]['name'] tooltip = self.custom_column_label_map[category]['name']
datatype = self.tag_browser_categories[category]['type'] datatype = self.tag_browser_categories[category]['type']
if datatype == 'rating': 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.))) formatter = (lambda x:u'\u2605'*int(round(x/2.)))
elif category == 'authors': 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 # Clean up the authors strings to human-readable form
formatter = (lambda x: x.replace('|', ',')) formatter = (lambda x: x.replace('|', ','))
else: else:
item_zero_func = (lambda x: x[2] > 0) item_not_zero_func = (lambda x: x[2] > 0)
formatter = (lambda x:x) formatter = (lambda x:x)
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
icon=icon, tooltip = tooltip) 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 # We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically # use a view, but is computed dynamically
@ -767,14 +770,14 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
items.append(taglist[label][name]) items.append(taglist[label][name])
# else: do nothing, to not include nodes w zero counts # else: do nothing, to not include nodes w zero counts
if len(items): 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] = { self.tag_browser_categories[cat_name] = {
'table':None, 'column':None, 'table':None, 'column':None,
'type':None, 'is_multiple':False, 'type':None, 'is_multiple':False,
'kind':'user', 'name':user_cat} 'kind':'user', 'name':user_cat}
# Not a problem if we accumulate entries in the icon map # Not a problem if we accumulate entries in the icon map
if icon_map is not None: if icon_map is not None:
icon_map[cat_name] = icon_map['*user'] icon_map[cat_name] = icon_map[':user']
if sort_on_count: if sort_on_count:
categories[cat_name] = \ categories[cat_name] = \
sorted(items, cmp=(lambda x, y: cmp(y.count, x.count))) sorted(items, cmp=(lambda x, y: cmp(y.count, x.count)))

View File

@ -22,7 +22,7 @@ from calibre.utils.pyparsing import Keyword, Group, Forward, CharsNotIn, Suppres
OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch, ParseException OneOrMore, oneOf, CaselessLiteral, Optional, NoMatch, ParseException
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.utils.config import prefs 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. This class manages access to the preference holding the saved search queries.
@ -87,21 +87,25 @@ class SearchQueryParser(object):
''' '''
DEFAULT_LOCATIONS = [ DEFAULT_LOCATIONS = [
'tag', 'all',
'title', 'author', # compatibility
'author', 'authors',
'comment', # compatibility
'comments',
'cover',
'date',
'format', # compatibility
'formats',
'isbn',
'ondevice',
'pubdate',
'publisher', 'publisher',
'search',
'series', 'series',
'rating', 'rating',
'cover', 'tag', # compatibility
'comments', 'tags',
'format', 'title',
'isbn',
'search',
'date',
'pubdate',
'ondevice',
'all',
] ]
@staticmethod @staticmethod
@ -118,6 +122,9 @@ class SearchQueryParser(object):
return failed return failed
def __init__(self, locations=None, test=False): 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: if locations is None:
locations = self.DEFAULT_LOCATIONS locations = self.DEFAULT_LOCATIONS
self._tests_failed = False self._tests_failed = False