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,