1) Ensure that Calibres always uses the same names for tag and search categories.

2) Add code to verify the names are reserved. 
3) add compatibility names to the reserved words list
4) fix model to show the correct search label 'data' for the timestamp column
5) change internal naming of icons and user categories to avoid namespace collisions
6) fix get_categories to do the right thing with rating fields
This commit is contained in:
Charles Haley 2010-05-25 14:29:51 +01:00
parent 9aebbc58d6
commit 3e926bb612
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