mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 02:34:06 -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):
|
||||
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()
|
||||
@ -135,8 +145,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
||||
{
|
||||
'isbn': '{identifiers:select(isbn)}',
|
||||
'formats': '{formats}',
|
||||
'last_modified':'''{last_modified:'format_date($, "%d %m, %Y")'}'''
|
||||
'last_modified':'''{last_modified:'format_date($, "dd MMM yy")'}'''
|
||||
}[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