mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Implement last modified custom column
This commit is contained in:
commit
105b7283b4
@ -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()
|
||||||
@ -135,8 +145,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
{
|
{
|
||||||
'isbn': '{identifiers:select(isbn)}',
|
'isbn': '{identifiers:select(isbn)}',
|
||||||
'formats': '{formats}',
|
'formats': '{formats}',
|
||||||
'last_modified':'''{last_modified:'format_date($, "%d %m, %Y")'}'''
|
'last_modified':'''{last_modified:'format_date($, "dd MMM yy")'}'''
|
||||||
}[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