From 701e553a5b68dc17c47f6dd336e20270d62cef8e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 3 Mar 2011 13:22:14 +0000 Subject: [PATCH] Several changes: 1) make last_modified work by correcting the attribute name in get_metadata 2) add a display attribute to composite columns to give them a subtype (sorting, searching) 3) change sort and search to use the subtype 4) change models.py to display composite columns with boolean decorators if subtype is bool --- src/calibre/gui2/library/models.py | 19 +++ .../gui2/preferences/create_custom_column.py | 18 ++- .../gui2/preferences/create_custom_column.ui | 50 ++++++- src/calibre/library/caches.py | 135 +++++++++++++----- src/calibre/library/database2.py | 2 +- src/calibre/utils/date.py | 2 + 6 files changed, 184 insertions(+), 42 deletions(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index a5e68ab6a6..1a8d4e93bc 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -616,6 +616,19 @@ class BooksModel(QAbstractTableModel): # {{{ def bool_type_decorator(r, idx=-1, bool_cols_are_tristate=True): val = self.db.data[r][idx] + if isinstance(val, (str, unicode)): + try: + val = icu_lower(val) + if not val: + val = None + elif val in [_('yes'), _('checked'), 'true']: + val = True + elif val in [_('no'), _('unchecked'), 'false']: + val = False + else: + val = bool(int(val)) + except: + val = None if not bool_cols_are_tristate: if val is None or not val: return self.bool_no_icon @@ -676,6 +689,12 @@ class BooksModel(QAbstractTableModel): # {{{ if datatype in ('text', 'comments', 'composite', 'enumeration'): self.dc[col] = functools.partial(text_type, idx=idx, mult=self.custom_columns[col]['is_multiple']) + if datatype == 'composite': + csort = self.custom_columns[col]['display'].get('composite_sort', 'text') + if csort == 'bool': + self.dc_decorator[col] = functools.partial( + bool_type_decorator, idx=idx, + bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] != 'no') elif datatype in ('int', 'float'): self.dc[col] = functools.partial(number_type, idx=idx) elif datatype == 'datetime': diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index d390763ab0..922717477b 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -68,6 +68,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): text = text[:-1] self.shortcuts.setText(text) + for sort_by in [_('Text'), _('Number'), _('Date'), _('Yes/No')]: + self.composite_sort_by.addItem(sort_by) + self.parent = parent self.editing_col = editing self.standard_colheads = standard_colheads @@ -108,6 +111,13 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): self.date_format_box.setText(c['display'].get('date_format', '')) elif ct == 'composite': self.composite_box.setText(c['display'].get('composite_template', '')) + sb = c['display'].get('composite_sort', 'text') + vals = ['text', 'number', 'date', 'bool'] + if sb in vals: + sb = vals.index(sb) + else: + sb = 0 + self.composite_sort_by.setCurrentIndex(sb) elif ct == 'enumeration': self.enum_box.setText(','.join(c['display'].get('enum_values', []))) self.datatype_changed() @@ -137,6 +147,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): 'formats': '{formats}', 'last_modified':'''{last_modified:'format_date($, "%d %m, %Y")'}''' }[which]) + self.composite_sort_by.setCurrentIndex(2 if which == 'last_modified' else 0) def datatype_changed(self, *args): @@ -146,7 +157,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): col_type = None for x in ('box', 'default_label', 'label'): getattr(self, 'date_format_'+x).setVisible(col_type == 'datetime') - for x in ('box', 'default_label', 'label'): + for x in ('box', 'default_label', 'label', 'sort_by', 'sort_by_label'): getattr(self, 'composite_'+x).setVisible(col_type == 'composite') for x in ('box', 'default_label', 'label'): getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration') @@ -201,7 +212,10 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): if not unicode(self.composite_box.text()).strip(): return self.simple_error('', _('You must enter a template for' ' composite columns')) - display_dict = {'composite_template':unicode(self.composite_box.text()).strip()} + display_dict = {'composite_template':unicode(self.composite_box.text()).strip(), + 'composite_sort': ['text', 'number', 'date', 'bool'] + [self.composite_sort_by.currentIndex()] + } elif col_type == 'enumeration': if not unicode(self.enum_box.text()).strip(): return self.simple_error('', _('You must enter at least one' diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui index 9df7107d9b..f045141ecb 100644 --- a/src/calibre/gui2/preferences/create_custom_column.ui +++ b/src/calibre/gui2/preferences/create_custom_column.ui @@ -80,7 +80,7 @@ - Column &type + &Column type column_type_box @@ -148,6 +148,16 @@ + + + + &Template + + + composite_box + + + @@ -175,16 +185,46 @@ - - + + - &Template + &Sort/search column by - composite_box + composite_sort_by + + + + + + How this column should handled in the GUI when sorting and searching + + + + + + + Qt::Horizontal + + + + 10 + 0 + + + + + 20 + 0 + + + + + + diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index dafeddaf86..823ef77bc5 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -302,14 +302,20 @@ class ResultCache(SearchQueryParser): # {{{ for id_ in candidates: item = self._data[id_] if item is None: continue - if item[loc] is None or item[loc] <= UNDEFINED_DATE: + v = item[loc] + if isinstance(v, (str, unicode)): + v = parse_date(v) + if v is None or v <= UNDEFINED_DATE: matches.add(item[0]) return matches if query == 'true': for id_ in candidates: item = self._data[id_] if item is None: continue - if item[loc] is not None and item[loc] > UNDEFINED_DATE: + v = item[loc] + if isinstance(v, (str, unicode)): + v = parse_date(v) + if v is not None and v > UNDEFINED_DATE: matches.add(item[0]) return matches @@ -349,7 +355,10 @@ class ResultCache(SearchQueryParser): # {{{ for id_ in candidates: item = self._data[id_] if item is None or item[loc] is None: continue - if relop(item[loc], qd, field_count): + v = item[loc] + if isinstance(v, (str, unicode)): + v = parse_date(v) + if relop(v, qd, field_count): matches.add(item[0]) return matches @@ -390,7 +399,7 @@ class ResultCache(SearchQueryParser): # {{{ elif dt == 'rating': cast = (lambda x: int (x)) adjust = lambda x: x/2 - elif dt == 'float': + elif dt in ('float', 'composite'): cast = lambda x : float (x) adjust = lambda x: x else: # count operation @@ -413,12 +422,15 @@ class ResultCache(SearchQueryParser): # {{{ item = self._data[id_] if item is None: continue - v = val_func(item) + try: + v = cast(val_func(item)) + except: + v = 0 if not v: - i = 0 + v = 0 else: - i = adjust(v) - if relop(i, q): + v = adjust(v) + if relop(v, q): matches.add(item[0]) return matches @@ -509,6 +521,50 @@ class ResultCache(SearchQueryParser): # {{{ query = icu_lower(query) return matchkind, query + def get_bool_matches(self, location, query, candidates): + bools_are_tristate = tweaks['bool_custom_columns_are_tristate'] != 'no' + loc = self.field_metadata[location]['rec_index'] + matches = set() + query = icu_lower(query) + for id_ in candidates: + item = self._data[id_] + if item is None: + continue + + val = item[loc] + if isinstance(val, (str, unicode)): + try: + val = icu_lower(val) + if not val: + val = None + elif val in [_('yes'), _('checked'), 'true']: + val = True + elif val in [_('no'), _('unchecked'), 'false']: + val = False + else: + val = bool(int(val)) + except: + val = None + + if not bools_are_tristate: + if val is None or not val: # item is None or set to false + if query in [_('no'), _('unchecked'), 'false']: + matches.add(item[0]) + else: # item is explicitly set to true + if query in [_('yes'), _('checked'), 'true']: + matches.add(item[0]) + else: + if val is None: + if query in [_('empty'), _('blank'), 'false']: + matches.add(item[0]) + elif not val: # is not None and false + if query in [_('no'), _('unchecked'), 'true']: + matches.add(item[0]) + else: # item is not None and true + if query in [_('yes'), _('checked'), 'true']: + matches.add(item[0]) + return matches + def get_matches(self, location, query, candidates=None, allow_recursion=True): matches = set([]) @@ -559,13 +615,20 @@ class ResultCache(SearchQueryParser): # {{{ if location in self.field_metadata: fm = self.field_metadata[location] # take care of dates special case - if fm['datatype'] == 'datetime': + if fm['datatype'] == 'datetime' or \ + (fm['datatype'] == 'composite' and + fm['display'].get('composite_sort', '') == 'date'): return self.get_dates_matches(location, query.lower(), candidates) # take care of numbers special case - if fm['datatype'] in ('rating', 'int', 'float'): + if fm['datatype'] in ('rating', 'int', 'float') or \ + (fm['datatype'] == 'composite' and + fm['display'].get('composite_sort', '') == 'number'): return self.get_numeric_matches(location, query.lower(), candidates) + if fm['datatype'] == 'bool': + return self.get_bool_matches(location, query, candidates) + # take care of the 'count' operator for is_multiples if fm['is_multiple'] and \ len(query) > 1 and query.startswith('#') and \ @@ -619,9 +682,6 @@ class ResultCache(SearchQueryParser): # {{{ for i, loc in enumerate(location): 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'] != 'no' - for loc in location: # location is now an array of field indices if loc == db_col['authors']: ### DB stores authors with commas changed to bars, so change query @@ -633,27 +693,6 @@ class ResultCache(SearchQueryParser): # {{{ item = self._data[id_] if item is None: continue - 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 - if q in [_('no'), _('unchecked'), 'false']: - matches.add(item[0]) - else: # item is explicitly set to true - if q in [_('yes'), _('checked'), 'true']: - matches.add(item[0]) - else: - if v is None: - if q in [_('empty'), _('blank'), 'false']: - matches.add(item[0]) - elif not v: # is not None and false - if q in [_('no'), _('unchecked'), 'true']: - matches.add(item[0]) - else: # item is not None and true - if q in [_('yes'), _('checked'), 'true']: - matches.add(item[0]) - continue - if not item[loc]: if q == 'false': matches.add(item[0]) @@ -893,6 +932,34 @@ class SortKeyGenerator(object): for name, fm in self.entries: dt = fm['datatype'] val = record[fm['rec_index']] + if dt == 'composite': + sb = fm['display'].get('composite_sort', 'text') + if sb == 'date': + try: + val = parse_date(val) + dt = 'datetime' + except: + pass + elif sb == 'number': + try: + val = float(val) + except: + val = 0.0 + dt = 'float' + elif sb == 'bool': + try: + v = icu_lower(val) + if not val: + val = None + elif v in [_('yes'), _('checked'), 'true']: + val = True + elif v in [_('no'), _('unchecked'), 'false']: + val = False + else: + val = bool(int(val)) + except: + val = None + dt = 'bool' if dt == 'datetime': if val is None: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index bb46411fc9..556131b2c9 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -833,7 +833,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): mi.pubdate = row[fm['pubdate']] mi.uuid = row[fm['uuid']] mi.title_sort = row[fm['sort']] - mi.metadata_last_modified = row[fm['last_modified']] + mi.last_modified = row[fm['last_modified']] formats = row[fm['formats']] if not formats: formats = None diff --git a/src/calibre/utils/date.py b/src/calibre/utils/date.py index eaf68df904..9b76a5a71a 100644 --- a/src/calibre/utils/date.py +++ b/src/calibre/utils/date.py @@ -71,6 +71,8 @@ def parse_date(date_string, assume_utc=False, as_utc=True, default=None): :param default: Missing fields are filled in from default. If None, the current date is used. ''' + if not date_string: + return UNDEFINED_DATE if default is None: func = datetime.utcnow if assume_utc else datetime.now default = func().replace(hour=0, minute=0, second=0, microsecond=0,