diff --git a/src/calibre/gui2/dialogs/tag_categories.py b/src/calibre/gui2/dialogs/tag_categories.py index fdec767d4d..50a0dccfb3 100644 --- a/src/calibre/gui2/dialogs/tag_categories.py +++ b/src/calibre/gui2/dialogs/tag_categories.py @@ -49,7 +49,7 @@ class TagCategories(QDialog, Ui_TagCategories): cc_map = self.db.custom_column_label_map for cc in cc_map: if cc_map[cc]['datatype'] == 'text': - self.category_labels.append(db.tag_browser_categories.get_search_label(cc)) + self.category_labels.append(db.tag_browser_categories.label_to_key(cc)) category_icons.append(cc_icon) category_values.append(lambda col=cc: self.db.all_custom(label=col)) category_names.append(cc_map[cc]['name']) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 36698533c5..20cfce02bd 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -157,7 +157,7 @@ class ResultCache(SearchQueryParser): self.first_sort = True self.search_restriction = '' self.tag_browser_categories = tag_browser_categories - self.all_search_locations = tag_browser_categories.get_search_labels() + self.all_search_locations = tag_browser_categories.get_search_keys() SearchQueryParser.__init__(self, self.all_search_locations) self.build_date_relop_dict() self.build_numeric_relop_dict() @@ -249,10 +249,10 @@ class ResultCache(SearchQueryParser): query = query[p:] if relop is None: (p, relop) = self.date_search_relops['='] - if location in self.custom_column_label_map: - loc = self.FIELD_MAP[self.custom_column_label_map[location]['num']] - else: - loc = self.FIELD_MAP[{'date':'timestamp', 'pubdate':'pubdate'}[location]] + + if location == 'date': + location = 'timestamp' + loc = self.tag_browser_categories[location]['rec_index'] if query == _('today'): qd = now() @@ -310,22 +310,18 @@ class ResultCache(SearchQueryParser): query = query[p:] if relop is None: (p, relop) = self.numeric_search_relops['='] - if location in self.custom_column_label_map: - loc = self.FIELD_MAP[self.custom_column_label_map[location]['num']] - dt = self.custom_column_label_map[location]['datatype'] - if dt == 'int': - cast = (lambda x: int (x)) - adjust = lambda x: x - elif dt == 'rating': - cast = (lambda x: int (x)) - adjust = lambda x: x/2 - elif dt == 'float': - cast = lambda x : float (x) - adjust = lambda x: x - else: - loc = self.FIELD_MAP['rating'] + + loc = self.tag_browser_categories[location]['rec_index'] + dt = self.tag_browser_categories[location]['datatype'] + if dt == 'int': + cast = (lambda x: int (x)) + adjust = lambda x: x + elif dt == 'rating': cast = (lambda x: int (x)) adjust = lambda x: x/2 + elif dt == 'float': + cast = lambda x : float (x) + adjust = lambda x: x try: q = cast(query) @@ -347,21 +343,22 @@ class ResultCache(SearchQueryParser): matches = set([]) if query and query.strip(): location = location.lower().strip() + if location in ('tag', 'author', 'format', 'comment'): + location += 's' ### take care of dates special case if (location in ('pubdate', 'date')) or \ - ((location in self.custom_column_label_map) and \ - self.custom_column_label_map[location]['datatype'] == 'datetime'): + (location in self.tag_browser_categories and \ + self.tag_browser_categories[location]['datatype'] == 'datetime'): return self.get_dates_matches(location, query.lower()) - ### take care of numerics special case - if location == 'rating' or \ - (location in self.custom_column_label_map and - self.custom_column_label_map[location]['datatype'] in - ('rating', 'int', 'float')): + ### take care of numbers special case + if location in self.tag_browser_categories and \ + self.tag_browser_categories[location]['datatype'] in \ + ('rating', 'int', 'float'): return self.get_numeric_matches(location, query.lower()) - ### everything else + ### everything else, or 'all' matches matchkind = CONTAINS_MATCH if (len(query) > 1): if query.startswith('\\'): @@ -372,57 +369,48 @@ class ResultCache(SearchQueryParser): elif query.startswith('~'): matchkind = REGEXP_MATCH query = query[1:] - if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D + if matchkind != REGEXP_MATCH: + # leave case in regexps because it can be significant e.g. \S \W \D query = query.lower() if not isinstance(query, unicode): query = query.decode('utf-8') - if location in ('tag', 'author', 'format', 'comment'): - location += 's' - MAP = {} - # Fields not used when matching against text contents. These are - # the non-text fields - EXCLUDE_FIELDS = [] - - # get the db columns for the standard searchables - for x in self.tag_browser_categories: - if len(self.tag_browser_categories[x]['search_labels']) and \ - not self.tag_browser_categories.is_custom_field(x): - MAP[x] = self.tag_browser_categories[x]['rec_index'] - if self.tag_browser_categories[x]['datatype'] != 'text': - EXCLUDE_FIELDS.append(MAP[x]) - - # add custom columns to MAP. Put the column's type into IS_CUSTOM - IS_CUSTOM = [] + db_col = {} + # fields to not check when matching against text. + exclude_fields = [] + col_datatype = [] for x in range(len(self.FIELD_MAP)): - IS_CUSTOM.append('') + col_datatype.append('') + for x in self.tag_browser_categories: + if len(self.tag_browser_categories[x]['search_keys']): + db_col[x] = self.tag_browser_categories[x]['rec_index'] + if self.tag_browser_categories[x]['datatype'] not in ['text', 'comments']: + exclude_fields.append(db_col[x]) + if self.tag_browser_categories.is_custom_field(x): + col_datatype[db_col[x]] = self.tag_browser_categories[x]['datatype'] # normal and custom ratings columns use the same code - IS_CUSTOM[self.FIELD_MAP['rating']] = 'rating' - for x in self.tag_browser_categories.get_custom_fields(): - if self.tag_browser_categories[x]['datatype'] != "datetime": - MAP[x] = self.FIELD_MAP[self.tag_browser_categories[x]['colnum']] - IS_CUSTOM[MAP[x]] = self.tag_browser_categories[x]['datatype'] + col_datatype[self.FIELD_MAP['rating']] = 'rating' - SPLITABLE_FIELDS = [MAP['authors'], MAP['tags'], MAP['formats']] + splitable_fields = [db_col['authors'], db_col['tags'], db_col['formats']] for x in self.tag_browser_categories.get_custom_fields(): if self.tag_browser_categories[x]['is_multiple']: - SPLITABLE_FIELDS.append(MAP[x]) + splitable_fields.append(db_col[x]) try: rating_query = int(query) * 2 except: rating_query = None - location = [location] if location != 'all' else list(MAP.keys()) + location = [location] if location != 'all' else list(db_col.keys()) for i, loc in enumerate(location): - location[i] = MAP[loc] + location[i] = db_col[loc] # get the tweak here so that the string lookup and compare aren't in the loop bools_are_tristate = tweaks['bool_custom_columns_are_tristate'] == 'yes' for loc in location: - if loc == MAP['authors']: + if loc == db_col['authors']: ### DB stores authors with commas changed to bars, so change query q = query.replace(',', '|'); else: @@ -431,7 +419,7 @@ class ResultCache(SearchQueryParser): for item in self._data: if item is None: continue - if IS_CUSTOM[loc] == 'bool': # complexity caused by the two-/three-value tweak + if col_datatype[loc] == 'bool': # complexity caused by the two-/three-value tweak v = item[loc] if not bools_are_tristate: if v is None or not v: # item is None or set to false @@ -466,18 +454,18 @@ class ResultCache(SearchQueryParser): matches.add(item[0]) continue - if IS_CUSTOM[loc] == 'rating': # get here if 'all' query + if col_datatype[loc] == 'rating': # get here if 'all' query if rating_query and rating_query == int(item[loc]): matches.add(item[0]) continue try: # a conversion below might fail # relationals not supported in 'all' queries - if IS_CUSTOM[loc] == 'float': + if col_datatype[loc] == 'float': if float(query) == item[loc]: matches.add(item[0]) continue - if IS_CUSTOM[loc] == 'int': + if col_datatype[loc] == 'int': if int(query) == item[loc]: matches.add(item[0]) continue @@ -486,9 +474,9 @@ class ResultCache(SearchQueryParser): # no further match is possible continue - if loc not in EXCLUDE_FIELDS: - if loc in SPLITABLE_FIELDS: - if IS_CUSTOM[loc]: + if loc not in exclude_fields: + if loc in splitable_fields: + if col_datatype[loc]: vals = item[loc].split('|') else: vals = item[loc].split(',') diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 535b8cfb72..5b5fcadf59 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -144,14 +144,14 @@ class CustomColumns(object): for k in sorted(self.custom_column_label_map.keys()): v = self.custom_column_label_map[k] if v['normalized']: - searchable = True + is_category = True else: - searchable = False + is_category = False tn = 'custom_column_{0}'.format(v['num']) self.tag_browser_categories.add_custom_field(label=v['label'], table=tn, column='value', datatype=v['datatype'], is_multiple=v['is_multiple'], colnum=v['num'], - name=v['name'], searchable=searchable) + name=v['name'], is_category=is_category) def get_custom(self, idx, label=None, num=None, index_is_id=False): if label is not None: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index a69d6bab57..939874ed3b 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -656,11 +656,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if icon_map is not None and type(icon_map) != TagsIcons: raise TypeError('icon_map passed to get_categories must be of type TagIcons') - #### First, build the standard and custom-column categories #### tb_cats = self.tag_browser_categories + + # remove all user categories from tag_browser_categories. They can + # easily come and go. We will add all the existing ones in below. + for k in tb_cats.keys(): + if tb_cats[k]['kind'] in ['user', 'search']: + del tb_cats[k] + + #### First, build the standard and custom-column categories #### for category in tb_cats.keys(): cat = tb_cats[category] - if cat['kind'] == 'not_cat': + if not cat['is_category']: continue tn = cat['table'] categories[category] = [] #reserve the position in the ordered list @@ -680,7 +687,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # icon_map is not None if get_categories is to store an icon and # possibly a tooltip in the tag structure. icon, tooltip = None, '' - label = tb_cats.get_field_label(category) + label = tb_cats.key_to_label(category) if icon_map: if not tb_cats.is_custom_field(category): if category in icon_map: @@ -737,12 +744,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): #### Now do the user-defined categories. #### user_categories = prefs['user_categories'] - # remove all user categories from tag_browser_categories. They can - # easily come and go. We will add all the existing ones in below. - for k in tb_cats.keys(): - if tb_cats[k]['kind'] in ['user', 'search']: - del tb_cats[k] - # We want to use same node in the user category as in the source # category. To do that, we need to find the original Tag node. There is # a time/space tradeoff here. By converting the tags into a map, we can diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 0134db712f..f8f3695473 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -4,7 +4,6 @@ Created on 25 May 2010 @author: charles ''' -from UserDict import DictMixin from calibre.utils.ordered_dict import OrderedDict class TagsIcons(dict): @@ -22,7 +21,7 @@ class TagsIcons(dict): raise ValueError('Missing category icon [%s]'%a) self[a] = icon_dict[a] -class FieldMetadata(dict, DictMixin): +class FieldMetadata(dict): # kind == standard: is tag category. May be a search label. Is db col # or is specially handled (e.g., news) @@ -41,86 +40,86 @@ class FieldMetadata(dict, DictMixin): ('authors', {'table':'authors', 'column':'name', 'datatype':'text', 'is_multiple':False, 'kind':'standard', 'name':_('Authors'), - 'search_labels':['authors', 'author'], - 'is_custom':False}), + 'search_keys':['authors', 'author'], + 'is_custom':False, 'is_category':True}), ('series', {'table':'series', 'column':'name', 'datatype':'text', 'is_multiple':False, 'kind':'standard', 'name':_('Series'), - 'search_labels':['series'], - 'is_custom':False}), + 'search_keys':['series'], + 'is_custom':False, 'is_category':True}), ('formats', {'table':None, 'column':None, 'datatype':'text', 'is_multiple':False, # must think what type this is! 'kind':'standard', 'name':_('Formats'), - 'search_labels':['formats', 'format'], - 'is_custom':False}), + 'search_keys':['formats', 'format'], + 'is_custom':False, 'is_category':True}), ('publisher', {'table':'publishers', 'column':'name', 'datatype':'text', 'is_multiple':False, 'kind':'standard', 'name':_('Publishers'), - 'search_labels':['publisher'], - 'is_custom':False}), + 'search_keys':['publisher'], + 'is_custom':False, 'is_category':True}), ('rating', {'table':'ratings', 'column':'rating', 'datatype':'rating', 'is_multiple':False, 'kind':'standard', 'name':_('Ratings'), - 'search_labels':['rating'], - 'is_custom':False}), + 'search_keys':['rating'], + 'is_custom':False, 'is_category':True}), ('news', {'table':'news', 'column':'name', 'datatype':None, 'is_multiple':False, 'kind':'standard', 'name':_('News'), - 'search_labels':[], - 'is_custom':False}), + 'search_keys':[], + 'is_custom':False, 'is_category':True}), ('tags', {'table':'tags', 'column':'name', 'datatype':'text', 'is_multiple':True, 'kind':'standard', 'name':_('Tags'), - 'search_labels':['tags', 'tag'], - 'is_custom':False}), + 'search_keys':['tags', 'tag'], + 'is_custom':False, 'is_category':True}), ('author_sort',{'table':None, 'column':None, 'datatype':'text', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ('comments', {'table':None, 'column':None, 'datatype':'text', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':['comments', 'comment'], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':['comments', 'comment'], 'is_custom':False, 'is_category':False}), ('cover', {'table':None, 'column':None, 'datatype':None, - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':['cover'], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':['cover'], 'is_custom':False, 'is_category':False}), ('flags', {'table':None, 'column':None, 'datatype':'text', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ('id', {'table':None, 'column':None, 'datatype':'int', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ('isbn', {'table':None, 'column':None, 'datatype':'text', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':['isbn'], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':['isbn'], 'is_custom':False, 'is_category':False}), ('lccn', {'table':None, 'column':None, 'datatype':'text', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ('ondevice', {'table':None, 'column':None, 'datatype':'bool', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ('path', {'table':None, 'column':None, 'datatype':'text', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ('pubdate', {'table':None, 'column':None, 'datatype':'datetime', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':['pubdate'], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':['pubdate'], 'is_custom':False, 'is_category':False}), ('series_index',{'table':None, 'column':None, 'datatype':'float', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ('sort', {'table':None, 'column':None, 'datatype':'text', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ('size', {'table':None, 'column':None, 'datatype':'float', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ('timestamp', {'table':None, 'column':None, 'datatype':'datetime', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':['date'], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':['date'], 'is_custom':False, 'is_category':False}), ('title', {'table':None, 'column':None, 'datatype':'text', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':['title'], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':['title'], 'is_custom':False, 'is_category':False}), ('uuid', {'table':None, 'column':None, 'datatype':'text', - 'is_multiple':False, 'kind':'not_cat', 'name':None, - 'search_labels':[], 'is_custom':False}), + 'is_multiple':False, 'kind':'standard', 'name':None, + 'search_keys':[], 'is_custom':False, 'is_category':False}), ] # search labels that are not db columns @@ -150,6 +149,12 @@ class FieldMetadata(dict, DictMixin): for key in self._tb_cats: yield key + def has_key(self, key): + return key in self._tb_cats + + def __contains__(self, key): + return self.has_key(key) + def keys(self): return self._tb_cats.keys() @@ -164,12 +169,12 @@ class FieldMetadata(dict, DictMixin): def is_custom_field(self, key): return key.startswith(self.custom_field_prefix) - def get_field_label(self, key): + def key_to_label(self, key): if 'label' not in self._tb_cats[key]: return key return self._tb_cats[key]['label'] - def get_search_label(self, label): + def label_to_key(self, label, prefer_custom=False): if 'label' in self._tb_cats: return label if self.is_custom_field(label): @@ -179,22 +184,17 @@ class FieldMetadata(dict, DictMixin): def get_custom_fields(self): return [l for l in self._tb_cats if self._tb_cats[l]['is_custom']] - def add_custom_field(self, label, table, column, datatype, - is_multiple, colnum, name, searchable): + def add_custom_field(self, label, table, column, datatype, colnum, + name, is_multiple, is_category): fn = self.custom_field_prefix + label if fn in self._tb_cats: raise ValueError('Duplicate custom field [%s]'%(label)) - if searchable: - sl = [fn] - kind = 'standard' - else: - sl = [] - kind = 'not_cat' self._tb_cats[fn] = {'table':table, 'column':column, 'datatype':datatype, 'is_multiple':is_multiple, - 'kind':kind, 'name':name, - 'search_labels':sl, 'label':label, - 'colnum':colnum, 'is_custom':True} + 'kind':'standard', 'name':name, + 'search_keys':[fn], 'label':label, + 'colnum':colnum, 'is_custom':True, + 'is_category':is_category} def set_field_record_index(self, label, index, prefer_custom=False): if prefer_custom: @@ -214,7 +214,8 @@ class FieldMetadata(dict, DictMixin): self._tb_cats[label] = {'table':None, 'column':None, 'datatype':None, 'is_multiple':False, 'kind':'user', 'name':name, - 'search_labels':[], 'is_custom':False} + 'search_keys':[], 'is_custom':False, + 'is_category':True} def add_search_category(self, label, name): if label in self._tb_cats: @@ -222,7 +223,8 @@ class FieldMetadata(dict, DictMixin): self._tb_cats[label] = {'table':None, 'column':None, 'datatype':None, 'is_multiple':False, 'kind':'search', 'name':name, - 'search_labels':[], 'is_custom':False} + 'search_keys':[], 'is_custom':False, + 'is_category':True} # DEFAULT_LOCATIONS = frozenset([ # 'all', @@ -247,13 +249,13 @@ class FieldMetadata(dict, DictMixin): # ]) - def get_search_labels(self): - s_labels = [] + def get_search_keys(self): + s_keys = [] for v in self._tb_cats.itervalues(): - map((lambda x:s_labels.append(x)), v['search_labels']) + map((lambda x:s_keys.append(x)), v['search_keys']) for v in self.search_items: - s_labels.append(v) -# if set(s_labels) != self.DEFAULT_LOCATIONS: + s_keys.append(v) +# if set(s_keys) != self.DEFAULT_LOCATIONS: # print 'search labels and default_locations do not match:' -# print set(s_labels) ^ self.DEFAULT_LOCATIONS - return s_labels +# print set(s_keys) ^ self.DEFAULT_LOCATIONS + return s_keys