mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
1bfcba90b6
commit
701e553a5b
@ -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':
|
||||||
|
@ -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'
|
||||||
|
@ -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 &type</string>
|
<string>&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>&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>&Template</string>
|
<string>&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">
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user