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):
|
||||
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':
|
||||
|
@ -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'
|
||||
|
@ -80,7 +80,7 @@
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Column &type</string>
|
||||
<string>&Column type</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>column_type_box</cstring>
|
||||
@ -148,6 +148,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</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">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
@ -175,16 +185,46 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="composite_label">
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="composite_sort_by_label">
|
||||
<property name="text">
|
||||
<string>&Template</string>
|
||||
<string>&Sort/search column by</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>composite_box</cstring>
|
||||
<cstring>composite_sort_by</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</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">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user