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
This commit is contained in:
Charles Haley 2011-03-03 13:22:14 +00:00
parent 1bfcba90b6
commit 701e553a5b
6 changed files with 184 additions and 42 deletions

View File

@ -616,6 +616,19 @@ class BooksModel(QAbstractTableModel): # {{{
def bool_type_decorator(r, idx=-1, bool_cols_are_tristate=True): def bool_type_decorator(r, idx=-1, bool_cols_are_tristate=True):
val = self.db.data[r][idx] 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 not bool_cols_are_tristate:
if val is None or not val: if val is None or not val:
return self.bool_no_icon return self.bool_no_icon
@ -676,6 +689,12 @@ class BooksModel(QAbstractTableModel): # {{{
if datatype in ('text', 'comments', 'composite', 'enumeration'): if datatype in ('text', 'comments', 'composite', 'enumeration'):
self.dc[col] = functools.partial(text_type, idx=idx, self.dc[col] = functools.partial(text_type, idx=idx,
mult=self.custom_columns[col]['is_multiple']) 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'): elif datatype in ('int', 'float'):
self.dc[col] = functools.partial(number_type, idx=idx) self.dc[col] = functools.partial(number_type, idx=idx)
elif datatype == 'datetime': elif datatype == 'datetime':

View File

@ -68,6 +68,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
text = text[:-1] text = text[:-1]
self.shortcuts.setText(text) self.shortcuts.setText(text)
for sort_by in [_('Text'), _('Number'), _('Date'), _('Yes/No')]:
self.composite_sort_by.addItem(sort_by)
self.parent = parent self.parent = parent
self.editing_col = editing self.editing_col = editing
self.standard_colheads = standard_colheads self.standard_colheads = standard_colheads
@ -108,6 +111,13 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
self.date_format_box.setText(c['display'].get('date_format', '')) self.date_format_box.setText(c['display'].get('date_format', ''))
elif ct == 'composite': elif ct == 'composite':
self.composite_box.setText(c['display'].get('composite_template', '')) 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': elif ct == 'enumeration':
self.enum_box.setText(','.join(c['display'].get('enum_values', []))) self.enum_box.setText(','.join(c['display'].get('enum_values', [])))
self.datatype_changed() self.datatype_changed()
@ -137,6 +147,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
'formats': '{formats}', 'formats': '{formats}',
'last_modified':'''{last_modified:'format_date($, "%d %m, %Y")'}''' 'last_modified':'''{last_modified:'format_date($, "%d %m, %Y")'}'''
}[which]) }[which])
self.composite_sort_by.setCurrentIndex(2 if which == 'last_modified' else 0)
def datatype_changed(self, *args): def datatype_changed(self, *args):
@ -146,7 +157,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
col_type = None col_type = None
for x in ('box', 'default_label', 'label'): for x in ('box', 'default_label', 'label'):
getattr(self, 'date_format_'+x).setVisible(col_type == 'datetime') 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') getattr(self, 'composite_'+x).setVisible(col_type == 'composite')
for x in ('box', 'default_label', 'label'): for x in ('box', 'default_label', 'label'):
getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration') 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(): if not unicode(self.composite_box.text()).strip():
return self.simple_error('', _('You must enter a template for' return self.simple_error('', _('You must enter a template for'
' composite columns')) ' 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': elif col_type == 'enumeration':
if not unicode(self.enum_box.text()).strip(): if not unicode(self.enum_box.text()).strip():
return self.simple_error('', _('You must enter at least one' return self.simple_error('', _('You must enter at least one'

View File

@ -80,7 +80,7 @@
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Column &amp;type</string> <string>&amp;Column type</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>column_type_box</cstring> <cstring>column_type_box</cstring>
@ -148,6 +148,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="0">
<widget class="QLabel" name="composite_label">
<property name="text">
<string>&amp;Template</string>
</property>
<property name="buddy">
<cstring>composite_box</cstring>
</property>
</widget>
</item>
<item row="5" column="2"> <item row="5" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_4"> <layout class="QHBoxLayout" name="horizontalLayout_4">
<item> <item>
@ -175,16 +185,46 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="5" column="0"> <item row="6" column="0">
<widget class="QLabel" name="composite_label"> <widget class="QLabel" name="composite_sort_by_label">
<property name="text"> <property name="text">
<string>&amp;Template</string> <string>&amp;Sort/search column by</string>
</property> </property>
<property name="buddy"> <property name="buddy">
<cstring>composite_box</cstring> <cstring>composite_sort_by</cstring>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QComboBox" name="composite_sort_by">
<property name="toolTip">
<string>How this column should handled in the GUI when sorting and searching</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_24">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>10</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="11" column="0" colspan="4"> <item row="11" column="0" colspan="4">
<spacer name="verticalSpacer_2"> <spacer name="verticalSpacer_2">
<property name="orientation"> <property name="orientation">

View File

@ -302,14 +302,20 @@ class ResultCache(SearchQueryParser): # {{{
for id_ in candidates: for id_ in candidates:
item = self._data[id_] item = self._data[id_]
if item is None: continue 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]) matches.add(item[0])
return matches return matches
if query == 'true': if query == 'true':
for id_ in candidates: for id_ in candidates:
item = self._data[id_] item = self._data[id_]
if item is None: continue 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]) matches.add(item[0])
return matches return matches
@ -349,7 +355,10 @@ class ResultCache(SearchQueryParser): # {{{
for id_ in candidates: for id_ in candidates:
item = self._data[id_] item = self._data[id_]
if item is None or item[loc] is None: continue 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]) matches.add(item[0])
return matches return matches
@ -390,7 +399,7 @@ class ResultCache(SearchQueryParser): # {{{
elif dt == 'rating': elif dt == 'rating':
cast = (lambda x: int (x)) cast = (lambda x: int (x))
adjust = lambda x: x/2 adjust = lambda x: x/2
elif dt == 'float': elif dt in ('float', 'composite'):
cast = lambda x : float (x) cast = lambda x : float (x)
adjust = lambda x: x adjust = lambda x: x
else: # count operation else: # count operation
@ -413,12 +422,15 @@ class ResultCache(SearchQueryParser): # {{{
item = self._data[id_] item = self._data[id_]
if item is None: if item is None:
continue continue
v = val_func(item) try:
v = cast(val_func(item))
except:
v = 0
if not v: if not v:
i = 0 v = 0
else: else:
i = adjust(v) v = adjust(v)
if relop(i, q): if relop(v, q):
matches.add(item[0]) matches.add(item[0])
return matches return matches
@ -509,6 +521,50 @@ class ResultCache(SearchQueryParser): # {{{
query = icu_lower(query) query = icu_lower(query)
return matchkind, 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, def get_matches(self, location, query, candidates=None,
allow_recursion=True): allow_recursion=True):
matches = set([]) matches = set([])
@ -559,13 +615,20 @@ class ResultCache(SearchQueryParser): # {{{
if location in self.field_metadata: if location in self.field_metadata:
fm = self.field_metadata[location] fm = self.field_metadata[location]
# take care of dates special case # 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) return self.get_dates_matches(location, query.lower(), candidates)
# take care of numbers special case # 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) 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 # take care of the 'count' operator for is_multiples
if fm['is_multiple'] and \ if fm['is_multiple'] and \
len(query) > 1 and query.startswith('#') and \ len(query) > 1 and query.startswith('#') and \
@ -619,9 +682,6 @@ class ResultCache(SearchQueryParser): # {{{
for i, loc in enumerate(location): for i, loc in enumerate(location):
location[i] = db_col[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'] != 'no'
for loc in location: # location is now an array of field indices for loc in location: # location is now an array of field indices
if loc == db_col['authors']: if loc == db_col['authors']:
### DB stores authors with commas changed to bars, so change query ### DB stores authors with commas changed to bars, so change query
@ -633,27 +693,6 @@ class ResultCache(SearchQueryParser): # {{{
item = self._data[id_] item = self._data[id_]
if item is None: continue 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 not item[loc]:
if q == 'false': if q == 'false':
matches.add(item[0]) matches.add(item[0])
@ -893,6 +932,34 @@ class SortKeyGenerator(object):
for name, fm in self.entries: for name, fm in self.entries:
dt = fm['datatype'] dt = fm['datatype']
val = record[fm['rec_index']] 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 dt == 'datetime':
if val is None: if val is None:

View File

@ -833,7 +833,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.pubdate = row[fm['pubdate']] mi.pubdate = row[fm['pubdate']]
mi.uuid = row[fm['uuid']] mi.uuid = row[fm['uuid']]
mi.title_sort = row[fm['sort']] 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']] formats = row[fm['formats']]
if not formats: if not formats:
formats = None formats = None

View File

@ -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 :param default: Missing fields are filled in from default. If None, the
current date is used. current date is used.
''' '''
if not date_string:
return UNDEFINED_DATE
if default is None: if default is None:
func = datetime.utcnow if assume_utc else datetime.now func = datetime.utcnow if assume_utc else datetime.now
default = func().replace(hour=0, minute=0, second=0, microsecond=0, default = func().replace(hour=0, minute=0, second=0, microsecond=0,