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: