diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py
index acfb5ca825..1cb6aad283 100644
--- a/src/calibre/gui2/dialogs/config/__init__.py
+++ b/src/calibre/gui2/dialogs/config/__init__.py
@@ -787,14 +787,17 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
label=c,
name=self.custcols[c]['name'],
datatype=self.custcols[c]['datatype'],
- is_multiple=self.custcols[c]['is_multiple'])
+ is_multiple=self.custcols[c]['is_multiple'],
+ display = self.custcols[c]['display'])
must_restart = True
elif '*deleteme' in self.custcols[c]:
self.db.delete_custom_column(label=c)
must_restart = True
elif '*edited' in self.custcols[c]:
cc = self.custcols[c]
- self.db.set_custom_column_metadata(cc['num'], name=cc['name'], label=cc['label'])
+ self.db.set_custom_column_metadata(cc['num'], name=cc['name'],
+ label=cc['label'],
+ display = self.custcols[c]['display'])
if '*must_restart' in self.custcols[c]:
must_restart = True
diff --git a/src/calibre/gui2/dialogs/config/create_custom_column.py b/src/calibre/gui2/dialogs/config/create_custom_column.py
index 03f8104223..8cfb01092b 100644
--- a/src/calibre/gui2/dialogs/config/create_custom_column.py
+++ b/src/calibre/gui2/dialogs/config/create_custom_column.py
@@ -48,18 +48,22 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
self.editing_col = editing
self.standard_colheads = standard_colheads
self.standard_colnames = standard_colnames
+ for t in self.column_types:
+ self.column_type_box.addItem(self.column_types[t]['text'])
+ self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
if not self.editing_col:
- for t in self.column_types:
- self.column_type_box.addItem(self.column_types[t]['text'])
+ self.datatype_changed()
self.exec_()
return
idx = parent.columns.currentRow()
if idx < 0:
- return self.simple_error(_('No column selected'),
+ self.simple_error(_('No column selected'),
_('No column has been selected'))
+ return
col = unicode(parent.columns.item(idx).data(Qt.UserRole).toString())
if col not in parent.custcols:
- return self.simple_error('', _('Selected column is not a user-defined column'))
+ self.simple_error('', _('Selected column is not a user-defined column'))
+ return
c = parent.custcols[col]
self.column_name_box.setText(c['label'])
@@ -68,9 +72,23 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
self.orig_column_number = c['num']
self.orig_column_name = col
column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x), self.column_types))
- self.column_type_box.addItem(self.column_types[column_numbers[ct]]['text'])
+ self.column_type_box.setCurrentIndex(column_numbers[ct])
+ self.column_type_box.setEnabled(False)
+ if ct == 'datetime':
+ self.date_format_box.setText(c['display'].get('date_format', ''))
+ self.datatype_changed()
self.exec_()
+ def datatype_changed(self, *args):
+ try:
+ col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
+ except:
+ col_type = None
+ df_visible = col_type == 'datetime'
+ for x in ('box', 'default_label', 'label'):
+ getattr(self, 'date_format_'+x).setVisible(df_visible)
+
+
def accept(self):
col = unicode(self.column_name_box.text())
col_heading = unicode(self.column_heading_box.text())
@@ -105,13 +123,18 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
if ':' in col or ' ' in col or col.lower() != col:
return self.simple_error('', _('The lookup name must be lower case and cannot contain ":"s or spaces'))
+ date_format = None
+ if col_type == 'datetime':
+ if self.date_format_box.text():
+ date_format = {'date_format':unicode(self.date_format_box.text())}
+
if not self.editing_col:
self.parent.custcols[col] = {
'label':col,
'name':col_heading,
'datatype':col_type,
'editable':True,
- 'display':None,
+ 'display':date_format,
'normalized':None,
'num':None,
'is_multiple':is_multiple,
@@ -127,6 +150,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
item.setText(col_heading)
self.parent.custcols[self.orig_column_name]['label'] = col
self.parent.custcols[self.orig_column_name]['name'] = col_heading
+ self.parent.custcols[self.orig_column_name]['display'] = date_format
self.parent.custcols[self.orig_column_name]['*edited'] = True
self.parent.custcols[self.orig_column_name]['*must_restart'] = True
QDialog.accept(self)
diff --git a/src/calibre/gui2/dialogs/config/create_custom_column.ui b/src/calibre/gui2/dialogs/config/create_custom_column.ui
index 247fbd9537..279349f28e 100644
--- a/src/calibre/gui2/dialogs/config/create_custom_column.ui
+++ b/src/calibre/gui2/dialogs/config/create_custom_column.ui
@@ -10,7 +10,7 @@
0
0
528
- 165
+ 199
@@ -33,6 +33,9 @@
-
+
+ 0
+
-
@@ -102,6 +105,48 @@
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ <p>Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 2 or 4 'y's for year.</p>
+<p>For example:
+<ul>
+<li> ddd, d MMM yyyy gives Mon, 5 Jan 2010<li>
+<li>dd MMMM yy gives 05 January 10</li>
+</ul>
+
+
+
+ -
+
+
+ Use MMM yyyy for month + year, yyyy for year only
+
+
+ Default: dd MMM yyyy.
+
+
+
+
+
+ -
+
+
+ Format for &dates
+
+
+ date_format_box
+
+
+
-
@@ -138,6 +183,7 @@
column_name_box
column_heading_box
column_type_box
+ date_format_box
button_box
diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py
index 896624c966..d2f99cea06 100644
--- a/src/calibre/gui2/library.py
+++ b/src/calibre/gui2/library.py
@@ -177,6 +177,33 @@ class TagsDelegate(QStyledItemDelegate):
editor = EnLineEdit(parent)
return editor
+class CcDateDelegate(QStyledItemDelegate):
+ '''
+ Delegate for custom columns dates. Because this delegate stores the
+ format as an instance variable, a new instance must be created for each
+ column. This differs from all the other delegates.
+ '''
+
+ def set_format(self, format):
+ if not format:
+ self.format = 'dd MMM yyyy'
+ else:
+ self.format = format
+
+ def displayText(self, val, locale):
+ d = val.toDate()
+ if d == UNDEFINED_DATE:
+ return ''
+ return d.toString(self.format)
+
+ def createEditor(self, parent, option, index):
+ qde = QStyledItemDelegate.createEditor(self, parent, option, index)
+ qde.setDisplayFormat(self.format)
+ qde.setMinimumDate(UNDEFINED_DATE)
+ qde.setSpecialValueText(_('Undefined'))
+ qde.setCalendarPopup(True)
+ return qde
+
class CcTextDelegate(QStyledItemDelegate):
'''
Delegate for text/int/float data.
@@ -989,7 +1016,9 @@ class BooksView(TableView):
continue
cc = self._model.custom_columns[colhead]
if cc['datatype'] == 'datetime':
- self.setItemDelegateForColumn(cm.index(colhead), self.timestamp_delegate)
+ delegate = CcDateDelegate(self)
+ delegate.set_format(cc['display'].get('date_format',''))
+ self.setItemDelegateForColumn(cm.index(colhead), delegate)
elif cc['datatype'] == 'comments':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
elif cc['datatype'] == 'text':
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index d792f693d2..55b6a00e99 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -159,7 +159,20 @@ class ResultCache(SearchQueryParser):
locations=SearchQueryParser.DEFAULT_LOCATIONS +
[c for c in cc_label_map])
self.build_date_relop_dict()
- self.build_rating_relop_dict()
+ self.build_numeric_relop_dict()
+
+ def __getitem__(self, row):
+ return self._data[self._map_filtered[row]]
+
+ def __len__(self):
+ return len(self._map_filtered)
+
+ def __iter__(self):
+ for id in self._map_filtered:
+ yield self._data[id]
+
+ def universal_set(self):
+ return set([i[0] for i in self._data if i is not None])
def build_date_relop_dict(self):
'''
@@ -205,30 +218,14 @@ class ResultCache(SearchQueryParser):
def relop_le(db, query, field_count):
return not relop_gt(db, query, field_count)
- self.date_search_relops = {'=':[1, relop_eq], '>':[1, relop_gt], '<':[1, relop_lt], \
- '!=':[2, relop_ne], '>=':[2, relop_ge], '<=':[2, relop_le]}
-
- def build_rating_relop_dict(self):
- self.rating_search_relops = {
- '=':[1, lambda r, q: r == q],
- '>':[1, lambda r, q: r > q],
- '<':[1, lambda r, q: r < q],
- '!=':[2, lambda r, q: r != q],
- '>=':[2, lambda r, q: r >= q],
- '<=':[2, lambda r, q: r <= q]}
-
- def __getitem__(self, row):
- return self._data[self._map_filtered[row]]
-
- def __len__(self):
- return len(self._map_filtered)
-
- def __iter__(self):
- for id in self._map_filtered:
- yield self._data[id]
-
- def universal_set(self):
- return set([i[0] for i in self._data if i is not None])
+ self.date_search_relops = {
+ '=' :[1, relop_eq],
+ '>' :[1, relop_gt],
+ '<' :[1, relop_lt],
+ '!=':[2, relop_ne],
+ '>=':[2, relop_ge],
+ '<=':[2, relop_le]
+ }
def get_dates_matches(self, location, query):
matches = set([])
@@ -277,7 +274,17 @@ class ResultCache(SearchQueryParser):
matches.add(item[0])
return matches
- def get_ratings_matches(self, location, query):
+ def build_numeric_relop_dict(self):
+ self.numeric_search_relops = {
+ '=':[1, lambda r, q: r == q],
+ '>':[1, lambda r, q: r > q],
+ '<':[1, lambda r, q: r < q],
+ '!=':[2, lambda r, q: r != q],
+ '>=':[2, lambda r, q: r >= q],
+ '<=':[2, lambda r, q: r <= q]
+ }
+
+ def get_numeric_matches(self, location, query):
matches = set([])
if len(query) == 0:
return matches
@@ -286,20 +293,33 @@ class ResultCache(SearchQueryParser):
elif query == 'true':
query = '>0'
relop = None
- for k in self.rating_search_relops.keys():
+ for k in self.numeric_search_relops.keys():
if query.startswith(k):
- (p, relop) = self.rating_search_relops[k]
+ (p, relop) = self.numeric_search_relops[k]
query = query[p:]
if relop is None:
- (p, relop) = self.rating_search_relops['=']
- try:
- r = int(query)
- except:
- return matches
+ (p, relop) = self.numeric_search_relops['=']
if location in self.custom_column_label_map:
loc = self.FIELD_MAP[self.custom_column_label_map[location]['num']]
+ dt = self.custom_column_label_map[location]['datatype']
+ if dt == 'int':
+ cast = (lambda x: int (x))
+ adjust = lambda x: x
+ elif dt == 'rating':
+ cast = (lambda x: int (x))
+ adjust = lambda x: x/2
+ elif dt == 'float':
+ cast = lambda x : float (x)
+ adjust = lambda x: x
else:
loc = self.FIELD_MAP['rating']
+ cast = (lambda x: int (x))
+ adjust = lambda x: x/2
+
+ try:
+ q = cast(query)
+ except:
+ return matches
for item in self._data:
if item is None:
@@ -307,8 +327,8 @@ class ResultCache(SearchQueryParser):
if not item[loc]:
i = 0
else:
- i = item[loc]/2
- if relop(i, r):
+ i = adjust(item[loc])
+ if relop(i, q):
matches.add(item[0])
return matches
@@ -323,11 +343,12 @@ class ResultCache(SearchQueryParser):
self.custom_column_label_map[location]['datatype'] == 'datetime'):
return self.get_dates_matches(location, query.lower())
- ### take care of ratings special case
+ ### take care of numerics special case
if location == 'rating' or \
- ((location in self.custom_column_label_map) and \
- self.custom_column_label_map[location]['datatype'] == 'rating'):
- return self.get_ratings_matches(location, query.lower())
+ (location in self.custom_column_label_map and
+ self.custom_column_label_map[location]['datatype'] in
+ ('rating', 'int', 'float')):
+ return self.get_numeric_matches(location, query.lower())
### everything else
matchkind = CONTAINS_MATCH
@@ -426,14 +447,15 @@ class ResultCache(SearchQueryParser):
matches.add(item[0])
continue
- if IS_CUSTOM[loc] == 'rating':
+ if IS_CUSTOM[loc] == 'rating': # get here if 'all' query
if rating_query and rating_query == int(item[loc]):
matches.add(item[0])
continue
try: # a conversion below might fail
+ # relationals not supported in 'all' queries
if IS_CUSTOM[loc] == 'float':
- if float(query) == item[loc]: # relationals not supported
+ if float(query) == item[loc]:
matches.add(item[0])
continue
if IS_CUSTOM[loc] == 'int':
@@ -441,7 +463,8 @@ class ResultCache(SearchQueryParser):
matches.add(item[0])
continue
except:
- # A conversion threw an exception. Because of the type, no further match possible
+ # A conversion threw an exception. Because of the type,
+ # no further match is possible
continue
if loc not in EXCLUDE_FIELDS: