From 7704ca4a0534885e66e4a0d371a46aff76d9159b Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 1 Dec 2010 19:07:29 +0000
Subject: [PATCH 1/8] First pass at enum custom type
---
src/calibre/gui2/custom_column_widgets.py | 82 +++++++++++++++++++
src/calibre/gui2/library/delegates.py | 33 ++++++++
src/calibre/gui2/library/models.py | 4 +-
src/calibre/gui2/library/views.py | 6 +-
.../gui2/preferences/create_custom_column.py | 17 +++-
.../gui2/preferences/create_custom_column.ui | 44 ++++++++--
src/calibre/library/custom_columns.py | 7 +-
src/calibre/library/field_metadata.py | 2 +-
8 files changed, 176 insertions(+), 19 deletions(-)
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 053dd7a743..7ea4fa679f 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -310,6 +310,37 @@ class Series(Base):
self.db.set_custom(book_id, val, extra=s_index,
num=self.col_id, notify=notify, commit=False)
+class Enumeration(Base):
+
+ def setup_ui(self, parent):
+ self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
+ QComboBox(parent)]
+ w = self.widgets[1]
+ print self.col_metadata['display']
+ vals = self.col_metadata['display']['enum_values']
+ w.addItem('')
+ for v in vals:
+ w.addItem(v)
+
+ def initialize(self, book_id):
+ val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
+ if val is None:
+ val = ''
+ self.initial_val = val
+ val = self.normalize_db_val(val)
+ idx = self.widgets[1].findText(val)
+ if idx < 0:
+ idx = 0
+ self.widgets[1].setCurrentIndex(idx)
+
+ def setter(self, val):
+ if val is None:
+ val = ''
+ self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
+
+ def getter(self):
+ return unicode(self.widgets[1].currentText())
+
widgets = {
'bool' : Bool,
'rating' : Rating,
@@ -319,6 +350,7 @@ widgets = {
'text' : Text,
'comments': Comments,
'series': Series,
+ 'enumeration': Enumeration
}
def field_sort(y, z, x=None):
@@ -551,6 +583,55 @@ class BulkSeries(BulkBase):
self.db.set_custom_bulk(book_ids, val, extras=extras,
num=self.col_id, notify=notify)
+class BulkEnumeration(BulkBase, Enumeration):
+
+ def get_initial_value(self, book_ids):
+ value = None
+ for book_id in book_ids:
+ val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
+ if value is not None and value != val:
+ return ' nochange '
+ value = val
+ return value
+
+ def setup_ui(self, parent):
+ self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
+ QComboBox(parent)]
+ w = self.widgets[1]
+ print self.col_metadata['display']
+ vals = self.col_metadata['display']['enum_values']
+ w.addItem('Do Not Change')
+ w.addItem('')
+ for v in vals:
+ w.addItem(v)
+
+ def getter(self):
+ if self.widgets[1].currentIndex() == 0:
+ return ' nochange '
+ return unicode(self.widgets[1].currentText())
+
+ def setter(self, val):
+ if val == ' nochange ':
+ self.widgets[1].setCurrentIndex(0)
+ else:
+ self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
+
+ def commit(self, book_ids, notify=False):
+ val = self.gui_val
+ val = self.normalize_ui_val(val)
+ if val != self.initial_val and val != ' nochange ':
+ self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
+
+ def normalize_ui_val(self, val):
+ if val == '':
+ return None
+ return val
+
+ def normalize_db_val(self, val):
+ if val is None:
+ return ''
+ return val
+
class RemoveTags(QWidget):
def __init__(self, parent, values):
@@ -656,4 +737,5 @@ bulk_widgets = {
'datetime': BulkDateTime,
'text' : BulkText,
'series': BulkSeries,
+ 'enumeration': BulkEnumeration,
}
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py
index f9ba612bc9..f5610bf849 100644
--- a/src/calibre/gui2/library/delegates.py
+++ b/src/calibre/gui2/library/delegates.py
@@ -254,6 +254,39 @@ class CcTextDelegate(QStyledItemDelegate): # {{{
# }}}
+class CcEnumDelegate(QStyledItemDelegate): # {{{
+ '''
+ Delegate for text/int/float data.
+ '''
+
+ def createEditor(self, parent, option, index):
+ m = index.model()
+ col = m.column_map[index.column()]
+ print 'delegate'
+ editor = QComboBox(parent)
+ editor.addItem('')
+ for v in m.custom_columns[col]['display']['enum_values']:
+ editor.addItem(v)
+ return editor
+
+ def setModelData(self, editor, model, index):
+ val = unicode(editor.currentText())
+ if val == '':
+ val = None
+ model.setData(index, QVariant(val), Qt.EditRole)
+
+ def setEditorData(self, editor, index):
+ m = index.model()
+ val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
+ if val is None:
+ val = ''
+ idx = editor.findText(val)
+ if idx < 0:
+ editor.setCurrentIndex(0)
+ else:
+ editor.setCurrentIndex(idx)
+# }}}
+
class CcCommentsDelegate(QStyledItemDelegate): # {{{
'''
Delegate for comments data.
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 4ef2c34c03..99639ed0a7 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -634,7 +634,7 @@ class BooksModel(QAbstractTableModel): # {{{
for col in self.custom_columns:
idx = self.custom_columns[col]['rec_index']
datatype = self.custom_columns[col]['datatype']
- if datatype in ('text', 'comments', 'composite'):
+ if datatype in ('text', 'comments', 'composite', 'enumeration'):
self.dc[col] = functools.partial(text_type, idx=idx,
mult=self.custom_columns[col]['is_multiple'])
elif datatype in ('int', 'float'):
@@ -719,7 +719,7 @@ class BooksModel(QAbstractTableModel): # {{{
typ = cc['datatype']
label=self.db.field_metadata.key_to_label(colhead)
s_index = None
- if typ in ('text', 'comments'):
+ if typ in ('text', 'comments', 'enumeration'):
val = unicode(value.toString()).strip()
val = val if val else None
if typ == 'bool':
diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py
index fadb8314e3..f724ca7b58 100644
--- a/src/calibre/gui2/library/views.py
+++ b/src/calibre/gui2/library/views.py
@@ -14,7 +14,8 @@ from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
- CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate
+ CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, \
+ CcEnumDelegate
from calibre.gui2.library.models import BooksModel, DeviceBooksModel
from calibre.utils.config import tweaks, prefs
from calibre.gui2 import error_dialog, gprefs
@@ -76,6 +77,7 @@ class BooksView(QTableView): # {{{
self.publisher_delegate = TextDelegate(self)
self.text_delegate = TextDelegate(self)
self.cc_text_delegate = CcTextDelegate(self)
+ self.cc_enum_delegate = CcEnumDelegate(self)
self.cc_bool_delegate = CcBoolDelegate(self)
self.cc_comments_delegate = CcCommentsDelegate(self)
self.cc_template_delegate = CcTemplateDelegate(self)
@@ -427,6 +429,8 @@ class BooksView(QTableView): # {{{
self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
elif cc['datatype'] == 'composite':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate)
+ elif cc['datatype'] == 'enumeration':
+ self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate)
else:
dattr = colhead+'_delegate'
delegate = colhead if hasattr(self, dattr) else 'text'
diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py
index ebf33784d4..d4d2b2678c 100644
--- a/src/calibre/gui2/preferences/create_custom_column.py
+++ b/src/calibre/gui2/preferences/create_custom_column.py
@@ -40,6 +40,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
'text':_('Yes/No'), 'is_multiple':False},
9:{'datatype':'composite',
'text':_('Column built from other columns'), 'is_multiple':False},
+ 10:{'datatype':'enumeration',
+ 'text':_('Fixed set of text values'), 'is_multiple':False},
}
def __init__(self, parent, editing, standard_colheads, standard_colnames):
@@ -90,7 +92,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
if c['display'].get('date_format', None):
self.date_format_box.setText(c['display'].get('date_format', ''))
elif ct == 'composite':
- self.composite_box.setText(c['display'].get('composite_template', ''))
+ self.composite_box.setText(c['display'].get('enum_values', ''))
self.datatype_changed()
self.exec_()
@@ -103,7 +105,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
getattr(self, 'date_format_'+x).setVisible(col_type == 'datetime')
for x in ('box', 'default_label', 'label'):
getattr(self, 'composite_'+x).setVisible(col_type == 'composite')
-
+ for x in ('box', 'default_label', 'label'):
+ getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
def accept(self):
col = unicode(self.column_name_box.text())
@@ -145,17 +148,23 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
return self.simple_error('', _('The heading %s is already used')%col_heading)
display_dict = {}
+
if col_type == 'datetime':
if self.date_format_box.text():
display_dict = {'date_format':unicode(self.date_format_box.text())}
else:
display_dict = {'date_format': None}
-
- if col_type == 'composite':
+ elif col_type == 'composite':
if not self.composite_box.text():
return self.simple_error('', _('You must enter a template for'
' composite columns'))
display_dict = {'composite_template':unicode(self.composite_box.text())}
+ elif col_type == 'enumeration':
+ if not self.enum_box.text():
+ return self.simple_error('', _('You must enter at least one'
+ ' value for enumeration columns'))
+ display_dict = {'enum_values':
+ [v.strip() for v in unicode(self.enum_box.text()).split(',')]}
db = self.parent.gui.library_view.model().db
key = db.field_metadata.custom_field_prefix+col
diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui
index 640becca8c..03c191d34e 100644
--- a/src/calibre/gui2/preferences/create_custom_column.ui
+++ b/src/calibre/gui2/preferences/create_custom_column.ui
@@ -10,7 +10,7 @@
0
0
528
- 199
+ 212
@@ -24,7 +24,7 @@
-
-
+
QLayout::SetDefaultConstraint
@@ -56,7 +56,7 @@
- -
+
-
@@ -69,7 +69,7 @@
- -
+
-
Column heading in the library view and category name in the tag browser
@@ -86,7 +86,7 @@
- -
+
-
@@ -105,7 +105,7 @@
- -
+
-
-
@@ -147,7 +147,7 @@
- -
+
-
-
@@ -184,7 +184,7 @@
- -
+
-
Qt::Vertical
@@ -197,6 +197,34 @@
+ -
+
+
+ Values
+
+
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ Default: (nothing)
+
+
+
+
+
-
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index fdd78e89f8..b477c89fe5 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -18,7 +18,7 @@ from calibre.utils.date import parse_date
class CustomColumns(object):
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
- 'int', 'float', 'bool', 'series', 'composite'])
+ 'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
def custom_table_names(self, num):
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
@@ -144,7 +144,8 @@ class CustomColumns(object):
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
'datetime' : adapt_datetime,
'text':adapt_text,
- 'series':adapt_text
+ 'series':adapt_text,
+ 'enumeration': adapt_text
}
# Create Tag Browser categories for custom columns
@@ -558,7 +559,7 @@ class CustomColumns(object):
if datatype in ('rating', 'int'):
dt = 'INT'
- elif datatype in ('text', 'comments', 'series', 'composite'):
+ elif datatype in ('text', 'comments', 'series', 'composite', 'enumeration'):
dt = 'TEXT'
elif datatype in ('float',):
dt = 'REAL'
diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py
index d10dc5da71..f5a156d3a7 100644
--- a/src/calibre/library/field_metadata.py
+++ b/src/calibre/library/field_metadata.py
@@ -83,7 +83,7 @@ class FieldMetadata(dict):
'''
VALID_DATA_TYPES = frozenset([None, 'rating', 'text', 'comments', 'datetime',
- 'int', 'float', 'bool', 'series', 'composite'])
+ 'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
# Builtin metadata {{{
From 882d16ffd19db7926eb0fc2457cec1269de1f75b Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 2 Dec 2010 09:15:11 +0000
Subject: [PATCH 2/8] Make advanced_search save history
---
src/calibre/gui2/search_box.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/gui2/search_box.py b/src/calibre/gui2/search_box.py
index 1cdf622537..c85dafc6d8 100644
--- a/src/calibre/gui2/search_box.py
+++ b/src/calibre/gui2/search_box.py
@@ -384,7 +384,7 @@ class SearchBoxMixin(object): # {{{
def do_advanced_search(self, *args):
d = SearchDialog(self, self.library_view.model().db)
if d.exec_() == QDialog.Accepted:
- self.search.set_search_string(d.search_string())
+ self.search.set_search_string(d.search_string(), store_in_history=True)
def do_search_button(self):
self.search.do_search()
From 6ba2d854fa5253900a61a04576ff5b91ffe78be4 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 2 Dec 2010 09:16:27 +0000
Subject: [PATCH 3/8] Fix #7740: CC ratings showing doubled on book info (and
mobile browser)
---
src/calibre/ebooks/metadata/book/base.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 125cd542b8..f0844e3711 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -531,6 +531,8 @@ class Metadata(object):
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool':
res = _('Yes') if res else _('No')
+ elif datatype == 'rating':
+ res = res/2
return (name, unicode(res), orig_res, cmeta)
# Translate aliases into the standard field name
From 438e0d5305c314c55768b6f3996a7c6f45a6226d Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 2 Dec 2010 09:16:45 +0000
Subject: [PATCH 4/8] Better enum-type help text
---
.../gui2/preferences/create_custom_column.py | 17 +++++++++--------
.../gui2/preferences/create_custom_column.ui | 5 ++++-
2 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py
index ff44080022..47fcb58afb 100644
--- a/src/calibre/gui2/preferences/create_custom_column.py
+++ b/src/calibre/gui2/preferences/create_custom_column.py
@@ -27,21 +27,21 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
3:{'datatype':'series',
'text':_('Text column for keeping series-like information'),
'is_multiple':False},
- 4:{'datatype':'datetime',
+ 4:{'datatype':'enumeration',
+ 'text':_('Text, but with a fixed set of permitted values'), 'is_multiple':False},
+ 5:{'datatype':'datetime',
'text':_('Date'), 'is_multiple':False},
- 5:{'datatype':'float',
+ 6:{'datatype':'float',
'text':_('Floating point numbers'), 'is_multiple':False},
- 6:{'datatype':'int',
+ 7:{'datatype':'int',
'text':_('Integers'), 'is_multiple':False},
- 7:{'datatype':'rating',
+ 8:{'datatype':'rating',
'text':_('Ratings, shown with stars'),
'is_multiple':False},
- 8:{'datatype':'bool',
+ 9:{'datatype':'bool',
'text':_('Yes/No'), 'is_multiple':False},
- 9:{'datatype':'composite',
+ 10:{'datatype':'composite',
'text':_('Column built from other columns'), 'is_multiple':False},
- 10:{'datatype':'enumeration',
- 'text':_('Fixed set of text values'), 'is_multiple':False},
}
def __init__(self, parent, editing, standard_colheads, standard_colnames):
@@ -61,6 +61,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
self.editing_col = editing
self.standard_colheads = standard_colheads
self.standard_colnames = standard_colnames
+ self.column_type_box.setMaxVisibleItems(len(self.column_types))
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)
diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui
index 3e4c6f59ca..1328f99690 100644
--- a/src/calibre/gui2/preferences/create_custom_column.ui
+++ b/src/calibre/gui2/preferences/create_custom_column.ui
@@ -212,7 +212,7 @@
-
- A comma-separated list of valid values.
+ A comma-separated list of permitted values. The empty value is always permitted
@@ -227,6 +227,9 @@
Default: (nothing)
+
+ Note that the empty value is always permitted, but is not shown in the list
+
From 5cdc87fc8962a830d940db7bfbf1756feb4ccfbe Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 2 Dec 2010 12:25:16 +0000
Subject: [PATCH 5/8] Enumeration type custom columns
---
src/calibre/gui2/custom_column_widgets.py | 42 +++++++++++++------
src/calibre/gui2/library/delegates.py | 12 ++++--
.../gui2/preferences/create_custom_column.ui | 4 +-
src/calibre/library/custom_columns.py | 39 +++++++++++------
src/calibre/library/sqlite.py | 13 ++++++
5 files changed, 78 insertions(+), 32 deletions(-)
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index 0b2065888c..b0b68033bb 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -15,7 +15,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
-from calibre.gui2 import UNDEFINED_QDATE
+from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.utils.config import tweaks
class Base(object):
@@ -313,22 +313,29 @@ class Series(Base):
class Enumeration(Base):
def setup_ui(self, parent):
+ self.parent = parent
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QComboBox(parent)]
w = self.widgets[1]
vals = self.col_metadata['display']['enum_values']
- w.addItem('')
for v in vals:
w.addItem(v)
def initialize(self, book_id):
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
if val is None:
- val = ''
+ # This really shouldn't happen
+ val = self.col_metadata['display']['enum_values'][0]
self.initial_val = val
val = self.normalize_db_val(val)
idx = self.widgets[1].findText(val)
if idx < 0:
+ error_dialog(self.parent, '',
+ _('The enumeration "{0}" contains an invalid value '
+ 'that will be set to the default').format(
+ self.col_metadata['name']),
+ show=True, show_copy_button=False)
+
idx = 0
self.widgets[1].setCurrentIndex(idx)
@@ -586,20 +593,33 @@ class BulkEnumeration(BulkBase, Enumeration):
def get_initial_value(self, book_ids):
value = None
+ ret_value = None
+ dialog_shown = False
for book_id in book_ids:
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
- if value is not None and value != val:
- return ' nochange '
+ if val not in self.col_metadata['display']['enum_values']:
+ if not dialog_shown:
+ error_dialog(self.parent, '',
+ _('The enumeration "{0}" contains invalid values '
+ 'that will not appear in the list').format(
+ self.col_metadata['name']),
+ show=True, show_copy_button=False)
+ dialog_shown = True
+ ret_value = ' nochange '
+ elif value is not None and value != val:
+ ret_value = ' nochange '
value = val
- return value
+ if ret_value is None:
+ return value
+ return ret_value
def setup_ui(self, parent):
+ self.parent = parent
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QComboBox(parent)]
w = self.widgets[1]
vals = self.col_metadata['display']['enum_values']
w.addItem('Do Not Change')
- w.addItem('')
for v in vals:
w.addItem(v)
@@ -620,14 +640,10 @@ class BulkEnumeration(BulkBase, Enumeration):
if val != self.initial_val and val != ' nochange ':
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
- def normalize_ui_val(self, val):
- if val == '':
- return None
- return val
-
def normalize_db_val(self, val):
if val is None:
- return ''
+ # this really shouldn't happen
+ val = self.col_metadata['display']['enum_values'][0]
return val
class RemoveTags(QWidget):
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py
index f5610bf849..73d722e485 100644
--- a/src/calibre/gui2/library/delegates.py
+++ b/src/calibre/gui2/library/delegates.py
@@ -262,9 +262,7 @@ class CcEnumDelegate(QStyledItemDelegate): # {{{
def createEditor(self, parent, option, index):
m = index.model()
col = m.column_map[index.column()]
- print 'delegate'
editor = QComboBox(parent)
- editor.addItem('')
for v in m.custom_columns[col]['display']['enum_values']:
editor.addItem(v)
return editor
@@ -272,14 +270,20 @@ class CcEnumDelegate(QStyledItemDelegate): # {{{
def setModelData(self, editor, model, index):
val = unicode(editor.currentText())
if val == '':
- val = None
+ # This shouldn't happen ...
+ m = index.model()
+ col = m.column_map[index.column()]
+ val = m.custom_columns[col]['display']['enum_values'][0]
model.setData(index, QVariant(val), Qt.EditRole)
def setEditorData(self, editor, index):
m = index.model()
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
if val is None:
- val = ''
+ # This shouldn't happen
+ m = index.model()
+ col = m.column_map[index.column()]
+ val = m.custom_columns[col]['display']['enum_values'][0]
idx = editor.findText(val)
if idx < 0:
editor.setCurrentIndex(0)
diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui
index 1328f99690..54003cd770 100644
--- a/src/calibre/gui2/preferences/create_custom_column.ui
+++ b/src/calibre/gui2/preferences/create_custom_column.ui
@@ -212,7 +212,7 @@
-
- A comma-separated list of permitted values. The empty value is always permitted
+ A comma-separated list of permitted values. The first value is the default
@@ -228,7 +228,7 @@
Default: (nothing)
- Note that the empty value is always permitted, but is not shown in the list
+ The first value entered will be the default value for this enumeration
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index 258b18e7ff..9444746c2a 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -177,6 +177,8 @@ class CustomColumns(object):
ans = ans.split('|') if ans else []
if data['display'].get('sort_alpha', False):
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
+ elif data['datatype'] == 'enumeration' and ans is None:
+ ans = data['display']['enum_values'][0]
return ans
def get_custom_extra(self, idx, label=None, num=None, index_is_id=False):
@@ -442,7 +444,6 @@ class CustomColumns(object):
if data['normalized']:
if data['datatype'] == 'enumeration' and \
val not in data['display']['enum_values']:
- print 'attempt to set enum to', val
return None
if not append or not data['is_multiple']:
self.conn.execute('DELETE FROM %s WHERE book=?'%lt, (id_,))
@@ -524,18 +525,30 @@ class CustomColumns(object):
display = data['display']
table, lt = self.custom_table_names(data['num'])
if data['normalized']:
- query = '%s.value'
- if data['is_multiple']:
- query = 'group_concat(%s.value, "|")'
- if not display.get('sort_alpha', False):
- query = 'sort_concat(link.id, %s.value)'
- line = '''(SELECT {query} FROM {lt} AS link INNER JOIN
- {table} ON(link.value={table}.id) WHERE link.book=books.id)
- custom_{num}
- '''.format(query=query%table, lt=lt, table=table, num=data['num'])
- if data['datatype'] == 'series':
- line += ''',(SELECT extra FROM {lt} WHERE {lt}.book=books.id)
- custom_index_{num}'''.format(lt=lt, num=data['num'])
+ if data['datatype'] == 'enumeration':
+ query = '%s.value'
+ line = '''
+ val_for_enum(\'
+ SELECT {table}.value FROM {lt}
+ AS link INNER JOIN {table} ON(link.value={table}.id)
+ WHERE link.book=?\',
+ \'{default}\', books.id) custom_{num}
+ '''.format(lt=lt, table=table,
+ default=data['display']['enum_values'][0],
+ num=data['num'])
+ else:
+ query = '%s.value'
+ if data['is_multiple']:
+ query = 'group_concat(%s.value, "|")'
+ if not display.get('sort_alpha', False):
+ query = 'sort_concat(link.id, %s.value)'
+ line = '''(SELECT {query} FROM {lt} AS link INNER JOIN
+ {table} ON(link.value={table}.id) WHERE link.book=books.id)
+ custom_{num}
+ '''.format(query=query%table, lt=lt, table=table, num=data['num'])
+ if data['datatype'] == 'series':
+ line += ''',(SELECT extra FROM {lt} WHERE {lt}.book=books.id)
+ custom_index_{num}'''.format(lt=lt, num=data['num'])
else:
line = '''
(SELECT value FROM {table} WHERE book=books.id) custom_{num}
diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py
index 8aa76b2643..9682bd7ec6 100644
--- a/src/calibre/library/sqlite.py
+++ b/src/calibre/library/sqlite.py
@@ -115,6 +115,17 @@ def pynocase(one, two, encoding='utf-8'):
pass
return cmp(one.lower(), two.lower())
+def enum_col_value(select, def_val, id, conn=None):
+ try:
+ v = conn.get(select, (id,), all=False)
+ if v is None:
+ v = def_val
+ except Exception, e:
+ if DEBUG:
+ print 'enum_col_value failed'
+ print e
+ v = def_val
+ return v
def load_c_extensions(conn, debug=DEBUG):
try:
@@ -165,6 +176,8 @@ class DBThread(Thread):
self.conn.create_function('author_to_author_sort', 1,
_author_to_author_sort)
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
+ self.conn.create_function('val_for_enum', 3,
+ partial(enum_col_value, conn=self.conn))
# Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1)
From da750b141071dab819145610a23d302181ac8ec8 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 2 Dec 2010 14:07:39 +0000
Subject: [PATCH 6/8] Some random changes: 1) added a 'count' function to the
template language 2) changed the template language faq 3) made author_sort
searchable 4) improved the help in calibredb create_custom_column
---
src/calibre/library/cli.py | 6 ++++--
src/calibre/library/field_metadata.py | 2 +-
src/calibre/manual/template_lang.rst | 5 +++--
src/calibre/utils/formatter.py | 13 +++++++++++--
4 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py
index 747cd59abb..5049476226 100644
--- a/src/calibre/library/cli.py
+++ b/src/calibre/library/cli.py
@@ -565,8 +565,9 @@ datatype is one of: {0}
'applies if datatype is text.'))
parser.add_option('--display', default='{}',
help=_('A dictionary of options to customize how '
- 'the data in this column will be interpreted.'))
-
+ 'the data in this column will be interpreted. This is a JSON '
+ ' string. For enumeration columns, use '
+ '--display=\'{"enum_values":["val1", "val2"]}\''))
return parser
@@ -579,6 +580,7 @@ def command_add_custom_column(args, dbpath):
print
print >>sys.stderr, _('You must specify label, name and datatype')
return 1
+ print opts.display
do_add_custom_column(get_db(dbpath, opts), args[0], args[1], args[2],
opts.is_multiple, json.loads(opts.display))
# Re-open the DB so that field_metadata is reflects the column changes
diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py
index f5a156d3a7..1be6604d5d 100644
--- a/src/calibre/library/field_metadata.py
+++ b/src/calibre/library/field_metadata.py
@@ -177,7 +177,7 @@ class FieldMetadata(dict):
'is_multiple':None,
'kind':'field',
'name':None,
- 'search_terms':[],
+ 'search_terms':['author_sort'],
'is_custom':False,
'is_category':False}),
('comments', {'table':None,
diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst
index e1eb876cb7..1bef32fbd6 100644
--- a/src/calibre/manual/template_lang.rst
+++ b/src/calibre/manual/template_lang.rst
@@ -119,10 +119,11 @@ The functions available are:
* ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`.
* ``test(text if not empty, text if empty)`` -- return `text if not empty` if the field is not empty, otherwise return `text if empty`.
* ``contains(pattern, text if match, text if not match`` -- checks if field contains matches for the regular expression `pattern`. Returns `text if match` if matches are found, otherwise it returns `text if no match`.
- * ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
+ * ``count(separator)`` -- interprets the value as a list of items separated by `separator`, returning the number of items in the list. Most lists use a comma as the separator, but authors uses an ampersand. Examples: `{tags:count(,)}`, `{authors:count(&)}`
+ * ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
* ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of |app|, these are python-compatible regular expressions.
* ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, consisting of `left chars` characters from the beginning of the field, followed by `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use ``{title:shorten(9,-,5)}``, the result will be `Ancient E-nhoe`. If the field's length is less than ``left chars`` + ``right chars`` + the length of ``middle text``, then the field will be used intact. For example, the title `The Dome` would not be changed.
- * ``lookup(pattern, field, pattern, field, ..., else_field)`` -- like switch, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
+ * ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
Now, about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be::
diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py
index 336ac2390b..15534a9c8a 100644
--- a/src/calibre/utils/formatter.py
+++ b/src/calibre/utils/formatter.py
@@ -79,6 +79,9 @@ class TemplateFormatter(string.Formatter):
else:
return val
+ def _count(self, val, sep):
+ return unicode(len(val.split(sep)))
+
functions = {
'uppercase' : (0, lambda s,x: x.upper()),
'lowercase' : (0, lambda s,x: x.lower()),
@@ -91,6 +94,7 @@ class TemplateFormatter(string.Formatter):
'shorten' : (3, _shorten),
'switch' : (-1, _switch),
'test' : (2, _test),
+ 'count' : (1, _count),
}
format_string_re = re.compile(r'^(.*)\|(.*)\|(.*)$')
@@ -136,8 +140,13 @@ class TemplateFormatter(string.Formatter):
if fmt[colon:p] in self.functions:
field = fmt[colon:p]
func = self.functions[field]
- args = self.arg_parser.scan(fmt[p+1:])[0]
- args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
+ if func[0] == 1:
+ # only one arg expected. Don't bother to scan. Avoids need
+ # for escaping characters
+ args = [fmt[p+1:-1]]
+ else:
+ args = self.arg_parser.scan(fmt[p+1:])[0]
+ args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
if (func[0] == 0 and (len(args) != 1 or args[0])) or \
(func[0] > 0 and func[0] != len(args)):
raise ValueError('Incorrect number of arguments for function '+ fmt[0:p])
From 46614c19e0feded0aad7f3fd39aa5730c8be8af4 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 2 Dec 2010 16:19:09 +0000
Subject: [PATCH 7/8] Fixes to allow an empty value in an enum
---
src/calibre/gui2/library/delegates.py | 7 ++++---
src/calibre/gui2/library/models.py | 6 ++++--
src/calibre/gui2/preferences/create_custom_column.py | 8 ++++++--
src/calibre/gui2/preferences/create_custom_column.ui | 5 ++++-
src/calibre/library/cli.py | 1 -
5 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py
index 73d722e485..bb73a55fc9 100644
--- a/src/calibre/gui2/library/delegates.py
+++ b/src/calibre/gui2/library/delegates.py
@@ -269,10 +269,11 @@ class CcEnumDelegate(QStyledItemDelegate): # {{{
def setModelData(self, editor, model, index):
val = unicode(editor.currentText())
- if val == '':
+ m = index.model()
+ col = m.column_map[index.column()]
+ if val not in m.custom_columns[col]['display']['enum_values']:
# This shouldn't happen ...
- m = index.model()
- col = m.column_map[index.column()]
+ print 'shouldnt happen'
val = m.custom_columns[col]['display']['enum_values'][0]
model.setData(index, QVariant(val), Qt.EditRole)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 99639ed0a7..bee90fc44c 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -719,10 +719,12 @@ class BooksModel(QAbstractTableModel): # {{{
typ = cc['datatype']
label=self.db.field_metadata.key_to_label(colhead)
s_index = None
- if typ in ('text', 'comments', 'enumeration'):
+ if typ in ('text', 'comments'):
val = unicode(value.toString()).strip()
val = val if val else None
- if typ == 'bool':
+ elif typ == 'enumeration':
+ val = unicode(value.toString()).strip()
+ elif typ == 'bool':
val = value.toPyObject()
elif typ == 'rating':
val = value.toInt()[0]
diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py
index 47fcb58afb..419fed046d 100644
--- a/src/calibre/gui2/preferences/create_custom_column.py
+++ b/src/calibre/gui2/preferences/create_custom_column.py
@@ -166,8 +166,12 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
if not self.enum_box.text():
return self.simple_error('', _('You must enter at least one'
' value for enumeration columns'))
- display_dict = {'enum_values':
- [v.strip() for v in unicode(self.enum_box.text()).split(',')]}
+ l = [v.strip() for v in unicode(self.enum_box.text()).split(',')]
+ for i in range(0, len(l)-1):
+ if l[i] in l[i+1:]:
+ return self.simple_error('', _('The value "{0}" is in the '
+ 'list more than once').format(l[i]))
+ display_dict = {'enum_values': l}
db = self.parent.gui.library_view.model().db
key = db.field_metadata.custom_field_prefix+col
diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui
index 54003cd770..360c1a4345 100644
--- a/src/calibre/gui2/preferences/create_custom_column.ui
+++ b/src/calibre/gui2/preferences/create_custom_column.ui
@@ -212,7 +212,10 @@
-
- A comma-separated list of permitted values. The first value is the default
+ A comma-separated list of permitted values. You can specify
+empty values by entering only the comma. For example, the list
+',one,two,three' has 4 valid values, one of them empty. The first
+value in the list is the default.
diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py
index 5049476226..01e8ad449b 100644
--- a/src/calibre/library/cli.py
+++ b/src/calibre/library/cli.py
@@ -580,7 +580,6 @@ def command_add_custom_column(args, dbpath):
print
print >>sys.stderr, _('You must specify label, name and datatype')
return 1
- print opts.display
do_add_custom_column(get_db(dbpath, opts), args[0], args[1], args[2],
opts.is_multiple, json.loads(opts.display))
# Re-open the DB so that field_metadata is reflects the column changes
From 5fa0b8941a2df6cfb5f7e46a77e79cbb0a23256b Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Thu, 2 Dec 2010 19:07:37 +0000
Subject: [PATCH 8/8] Change enums to not use the user defined function in
meta2
---
src/calibre/library/caches.py | 18 ++++++++++++++
src/calibre/library/custom_columns.py | 36 +++++++++------------------
src/calibre/library/sqlite.py | 14 -----------
3 files changed, 30 insertions(+), 38 deletions(-)
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index 7b4c66c8b8..8365cfe773 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -254,6 +254,12 @@ class ResultCache(SearchQueryParser): # {{{
if field_metadata[key]['datatype'] == 'composite':
self.composites.append((key, field_metadata[key]['rec_index']))
+ self.enumerations = []
+ for key in field_metadata:
+ if field_metadata[key]['datatype'] == 'enumeration':
+ self.enumerations.append((field_metadata[key]['display']['enum_values'][0],
+ field_metadata[key]['rec_index']))
+
def __getitem__(self, row):
return self._data[self._map_filtered[row]]
@@ -691,6 +697,10 @@ class ResultCache(SearchQueryParser): # {{{
mi = db.get_metadata(id, index_is_id=True)
for k,c in self.composites:
self._data[id][c] = mi.get(k, None)
+ if len(self.enumerations) > 0:
+ for v,c in self.enumerations:
+ if self._data[id][c] is None:
+ self._data[id][c] = v
except IndexError:
return None
try:
@@ -711,6 +721,10 @@ class ResultCache(SearchQueryParser): # {{{
mi = db.get_metadata(id, index_is_id=True)
for k,c in self.composites:
self._data[id][c] = mi.get(k)
+ if len(self.enumerations) > 0:
+ for v,c in self.self._data[id][c]:
+ if self._data[id][c] is None:
+ self._data[id][c] = v
self._map[0:0] = ids
self._map_filtered[0:0] = ids
@@ -740,6 +754,10 @@ class ResultCache(SearchQueryParser): # {{{
mi = db.get_metadata(item[0], index_is_id=True)
for k,c in self.composites:
item[c] = mi.get(k)
+ if len(self.enumerations) > 0:
+ for v,c in self.enumerations:
+ if item[c] is None:
+ item[c] = v
self._map = [i[0] for i in self._data if i is not None]
if field is not None:
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index 9444746c2a..dc3a67e860 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -525,30 +525,18 @@ class CustomColumns(object):
display = data['display']
table, lt = self.custom_table_names(data['num'])
if data['normalized']:
- if data['datatype'] == 'enumeration':
- query = '%s.value'
- line = '''
- val_for_enum(\'
- SELECT {table}.value FROM {lt}
- AS link INNER JOIN {table} ON(link.value={table}.id)
- WHERE link.book=?\',
- \'{default}\', books.id) custom_{num}
- '''.format(lt=lt, table=table,
- default=data['display']['enum_values'][0],
- num=data['num'])
- else:
- query = '%s.value'
- if data['is_multiple']:
- query = 'group_concat(%s.value, "|")'
- if not display.get('sort_alpha', False):
- query = 'sort_concat(link.id, %s.value)'
- line = '''(SELECT {query} FROM {lt} AS link INNER JOIN
- {table} ON(link.value={table}.id) WHERE link.book=books.id)
- custom_{num}
- '''.format(query=query%table, lt=lt, table=table, num=data['num'])
- if data['datatype'] == 'series':
- line += ''',(SELECT extra FROM {lt} WHERE {lt}.book=books.id)
- custom_index_{num}'''.format(lt=lt, num=data['num'])
+ query = '%s.value'
+ if data['is_multiple']:
+ query = 'group_concat(%s.value, "|")'
+ if not display.get('sort_alpha', False):
+ query = 'sort_concat(link.id, %s.value)'
+ line = '''(SELECT {query} FROM {lt} AS link INNER JOIN
+ {table} ON(link.value={table}.id) WHERE link.book=books.id)
+ custom_{num}
+ '''.format(query=query%table, lt=lt, table=table, num=data['num'])
+ if data['datatype'] == 'series':
+ line += ''',(SELECT extra FROM {lt} WHERE {lt}.book=books.id)
+ custom_index_{num}'''.format(lt=lt, num=data['num'])
else:
line = '''
(SELECT value FROM {table} WHERE book=books.id) custom_{num}
diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py
index 9682bd7ec6..7a86447090 100644
--- a/src/calibre/library/sqlite.py
+++ b/src/calibre/library/sqlite.py
@@ -115,18 +115,6 @@ def pynocase(one, two, encoding='utf-8'):
pass
return cmp(one.lower(), two.lower())
-def enum_col_value(select, def_val, id, conn=None):
- try:
- v = conn.get(select, (id,), all=False)
- if v is None:
- v = def_val
- except Exception, e:
- if DEBUG:
- print 'enum_col_value failed'
- print e
- v = def_val
- return v
-
def load_c_extensions(conn, debug=DEBUG):
try:
conn.enable_load_extension(True)
@@ -176,8 +164,6 @@ class DBThread(Thread):
self.conn.create_function('author_to_author_sort', 1,
_author_to_author_sort)
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
- self.conn.create_function('val_for_enum', 3,
- partial(enum_col_value, conn=self.conn))
# Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1)