From 1c1e3bf9a163a4a288b8e2b59cb926bf11b68d98 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 7 May 2010 17:36:40 +0100 Subject: [PATCH 1/2] Permit customization of date display. The format string can be anything accepted by QDate, with a default of dd MMM yyyy (set in library.py): d the day as number without a leading zero (1 to 31) dd the day as number with a leading zero (01 to 31) ddd the abbreviated localized day name (e.g. 'Mon' to 'Sun'). dddd the long localized day name (e.g. 'Monday' to 'Sunday'). M the month as number without a leading zero (1 to 12) MM the month as number with a leading zero (01 to 12) MMM the abbreviated localized month name (e.g. 'Jan' to 'Dec'). MMMM the long localized month name (e.g. 'January' to 'December'). yy the year as two digit number (00 to 99) yyyy the year as four digit number. If the year is negative, a minus sign is prepended in addition. --- src/calibre/gui2/dialogs/config/__init__.py | 7 ++- .../dialogs/config/create_custom_column.py | 17 ++++++-- .../dialogs/config/create_custom_column.ui | 43 ++++++++++++++++++- src/calibre/gui2/library.py | 31 ++++++++++++- 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index dc1ca8111e..cbe53662d9 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -776,14 +776,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..56ae592378 100644 --- a/src/calibre/gui2/dialogs/config/create_custom_column.py +++ b/src/calibre/gui2/dialogs/config/create_custom_column.py @@ -48,9 +48,9 @@ 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']) if not self.editing_col: - for t in self.column_types: - self.column_type_box.addItem(self.column_types[t]['text']) self.exec_() return idx = parent.columns.currentRow() @@ -68,7 +68,10 @@ 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.exec_() def accept(self): @@ -105,13 +108,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 +135,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..2079fb4930 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 + 194 @@ -33,6 +33,9 @@ + + 0 + @@ -102,6 +105,43 @@ + + + + + + + 0 + 0 + + + + Date format. Use 1-4 'd's for day, 1-4 'M's for month, and 1-2 'Y's for year. + + + + + + + Use MMM yyyy for month + year, yyyy for year only + + + Default: dd MMM yyyy. + + + + + + + + + Format for dates + + + date_format_box + + + @@ -138,6 +178,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': From aab34ff1972b7a3ecca95f82f835b67997d0a7d3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 7 May 2010 20:13:12 +0100 Subject: [PATCH 2/2] Implement enhancement #5481, relational searches for int and float custom columns --- src/calibre/library/caches.py | 107 +++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 42 deletions(-) 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: