diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py
index cfebe796a3..731d3e2b49 100644
--- a/src/calibre/devices/usbms/books.py
+++ b/src/calibre/devices/usbms/books.py
@@ -204,7 +204,8 @@ class CollectionsBookList(BookList):
elif fm['datatype'] == 'text' and fm['is_multiple']:
val = orig_val
elif fm['datatype'] == 'composite' and fm['is_multiple']:
- val = [v.strip() for v in val.split(fm['is_multiple'])]
+ val = [v.strip() for v in
+ val.split(fm['is_multiple']['ui_to_list'])]
else:
val = [val]
diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py
index 378d4ab5f0..382cb6c5a2 100644
--- a/src/calibre/ebooks/metadata/book/base.py
+++ b/src/calibre/ebooks/metadata/book/base.py
@@ -621,10 +621,7 @@ class Metadata(object):
orig_res = res
datatype = cmeta['datatype']
if datatype == 'text' and cmeta['is_multiple']:
- if cmeta['display'].get('is_names', False):
- res = u' & '.join(res)
- else:
- res = u', '.join(sorted(res, key=sort_key))
+ res = cmeta['is_multiple']['list_to_ui'].join(res)
elif datatype == 'series' and series_with_index:
if self.get_extra(key) is not None:
res = res + \
@@ -668,7 +665,7 @@ class Metadata(object):
elif datatype == 'text' and fmeta['is_multiple']:
if isinstance(res, dict):
res = [k + ':' + v for k,v in res.items()]
- res = u', '.join(sorted(res, key=sort_key))
+ res = fmeta['is_multiple']['list_to_ui'].join(sorted(res, key=sort_key))
elif datatype == 'series' and series_with_index:
res = res + ' [%s]'%self.format_series_index()
elif datatype == 'datetime':
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 1d91236757..b83e0f5177 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
lxml based OPF parser.
'''
-import re, sys, unittest, functools, os, uuid, glob, cStringIO, json
+import re, sys, unittest, functools, os, uuid, glob, cStringIO, json, copy
from urllib import unquote
from urlparse import urlparse
@@ -457,6 +457,14 @@ def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8))
for name, fm in all_user_metadata.items():
try:
+ if fm.get('is_multiple'):
+ # migrate is_multiple back to a character
+ fm = copy.copy(fm)
+ dt = fm.get('datatype', None)
+ if dt == 'composite':
+ fm['is_multiple'] = ','
+ else:
+ fm['is_multiple'] = '|'
fm = object_to_unicode(fm)
fm = json.dumps(fm, default=to_json, ensure_ascii=False)
except:
@@ -585,6 +593,17 @@ class OPF(object): # {{{
fm = elem.get('content')
try:
fm = json.loads(fm, object_hook=from_json)
+ im = fm.get('is_multiple', None)
+ if im and not isinstance(im, dict):
+ # Must migrate the is_multiple from char to dict
+ dt = fm.get('datatype', None)
+ if dt == 'composite':
+ im = {'cache_to_list': ',', 'ui_to_list': ',', 'list_to_ui': ', '}
+ elif fm.get('display', {}).get('is_names', False):
+ im = {'cache_to_list': '|', 'ui_to_list': '&', 'list_to_ui': ', '}
+ else:
+ im = {'cache_to_list': '|', 'ui_to_list': ',', 'list_to_ui': ', '}
+ fm['is_multiple'] = im
temp.set_user_metadata(name, fm)
except:
prints('Failed to read user metadata:', name)
diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py
index c94913ea2c..4706cce4c9 100644
--- a/src/calibre/gui2/custom_column_widgets.py
+++ b/src/calibre/gui2/custom_column_widgets.py
@@ -226,16 +226,14 @@ class Comments(Base):
class Text(Base):
def setup_ui(self, parent):
- if self.col_metadata['display'].get('is_names', False):
- self.sep = u' & '
- else:
- self.sep = u', '
+ self.sep = self.col_metadata['multiple_seps']
values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key)
+
if self.col_metadata['is_multiple']:
w = MultiCompleteLineEdit(parent)
- w.set_separator(self.sep.strip())
- if self.sep == u' & ':
+ w.set_separator(self.sep['ui_to_list'])
+ if self.sep['ui_to_list'] == '&':
w.set_space_before_sep(True)
w.set_add_separator(tweaks['authors_completer_append_separator'])
w.update_items_cache(values)
@@ -269,12 +267,12 @@ class Text(Base):
if self.col_metadata['is_multiple']:
if not val:
val = []
- self.widgets[1].setText(self.sep.join(val))
+ self.widgets[1].setText(self.sep['list_to_ui'].join(val))
def getter(self):
if self.col_metadata['is_multiple']:
val = unicode(self.widgets[1].text()).strip()
- ans = [x.strip() for x in val.split(self.sep.strip()) if x.strip()]
+ ans = [x.strip() for x in val.split(self.sep['ui_to_list']) if x.strip()]
if not ans:
ans = None
return ans
@@ -899,9 +897,10 @@ class BulkText(BulkBase):
if not self.a_c_checkbox.isChecked():
return
if self.col_metadata['is_multiple']:
+ ism = self.col_metadata['multiple_seps']
if self.col_metadata['display'].get('is_names', False):
val = self.gui_val
- add = [v.strip() for v in val.split('&') if v.strip()]
+ add = [v.strip() for v in val.split(ism['ui_to_list']) if v.strip()]
self.db.set_custom_bulk(book_ids, add, num=self.col_id)
else:
remove_all, adding, rtext = self.gui_val
@@ -911,10 +910,10 @@ class BulkText(BulkBase):
else:
txt = rtext
if txt:
- remove = set([v.strip() for v in txt.split(',')])
+ remove = set([v.strip() for v in txt.split(ism['ui_to_list'])])
txt = adding
if txt:
- add = set([v.strip() for v in txt.split(',')])
+ add = set([v.strip() for v in txt.split(ism['ui_to_list'])])
else:
add = set()
self.db.set_custom_bulk_multiple(book_ids, add=add,
diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py
index 66cf55a9b2..8829dc97c0 100644
--- a/src/calibre/gui2/dialogs/metadata_bulk.py
+++ b/src/calibre/gui2/dialogs/metadata_bulk.py
@@ -520,7 +520,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
elif not fm['is_multiple']:
val = [val]
elif fm['datatype'] == 'composite':
- val = [v.strip() for v in val.split(fm['is_multiple'])]
+ val = [v.strip() for v in val.split(fm['is_multiple']['ui_to_list'])]
elif field == 'authors':
val = [v.replace('|', ',') for v in val]
else:
@@ -655,19 +655,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
if self.destination_field_fm['is_multiple']:
if self.comma_separated.isChecked():
- if dest == 'authors' or \
- (self.destination_field_fm['is_custom'] and
- self.destination_field_fm['datatype'] == 'text' and
- self.destination_field_fm['display'].get('is_names', False)):
- splitter = ' & '
- else:
- splitter = ','
-
+ splitter = self.destination_field_fm['is_multiple']['ui_to_list']
res = []
for v in val:
- for x in v.split(splitter):
- if x.strip():
- res.append(x.strip())
+ res.extend([x.strip() for x in v.split(splitter) if x.strip()])
val = res
else:
val = [v.replace(',', '') for v in val]
diff --git a/src/calibre/gui2/dialogs/template_dialog.py b/src/calibre/gui2/dialogs/template_dialog.py
index 852bbcc221..f78e7a7383 100644
--- a/src/calibre/gui2/dialogs/template_dialog.py
+++ b/src/calibre/gui2/dialogs/template_dialog.py
@@ -254,6 +254,15 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.textbox_changed()
self.rule = (None, '')
+ tt = _('Template language tutorial')
+ self.template_tutorial.setText(
+ ''
+ '%s'%tt)
+ tt = _('Template function reference')
+ self.template_func_reference.setText(
+ ''
+ '%s'%tt)
+
def textbox_changed(self):
cur_text = unicode(self.textbox.toPlainText())
if self.last_text != cur_text:
@@ -299,4 +308,4 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
return
self.rule = (unicode(self.colored_field.currentText()), txt)
- QDialog.accept(self)
\ No newline at end of file
+ QDialog.accept(self)
diff --git a/src/calibre/gui2/dialogs/template_dialog.ui b/src/calibre/gui2/dialogs/template_dialog.ui
index 13586e7049..674100fe04 100644
--- a/src/calibre/gui2/dialogs/template_dialog.ui
+++ b/src/calibre/gui2/dialogs/template_dialog.ui
@@ -125,6 +125,20 @@
-
+ -
+
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index 72c8e0629f..72655afd12 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -608,10 +608,11 @@ class BooksModel(QAbstractTableModel): # {{{
def text_type(r, mult=None, idx=-1):
text = self.db.data[r][idx]
- if text and mult is not None:
- if mult:
- return QVariant(u' & '.join(text.split('|')))
- return QVariant(u', '.join(sorted(text.split('|'),key=sort_key)))
+ if text and mult:
+ jv = mult['list_to_ui']
+ sv = mult['cache_to_list']
+ return QVariant(jv.join(
+ sorted([t.strip() for t in text.split(sv)], key=sort_key)))
return QVariant(text)
def decorated_text_type(r, idx=-1):
@@ -665,8 +666,6 @@ class BooksModel(QAbstractTableModel): # {{{
datatype = self.custom_columns[col]['datatype']
if datatype in ('text', 'comments', 'composite', 'enumeration'):
mult=self.custom_columns[col]['is_multiple']
- if mult is not None:
- mult = self.custom_columns[col]['display'].get('is_names', False)
self.dc[col] = functools.partial(text_type, idx=idx, mult=mult)
if datatype in ['text', 'composite', 'enumeration'] and not mult:
if self.custom_columns[col]['display'].get('use_decorations', False):
@@ -722,9 +721,9 @@ class BooksModel(QAbstractTableModel): # {{{
if id_ in self.color_cache:
if key in self.color_cache[id_]:
return self.color_cache[id_][key]
- if mi is None:
- mi = self.db.get_metadata(id_, index_is_id=True)
try:
+ if mi is None:
+ mi = self.db.get_metadata(id_, index_is_id=True)
color = composite_formatter.safe_format(fmt, mi, '', mi)
if color in self.colors:
color = QColor(color)
diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py
index 8a13ead516..f8376e9b84 100644
--- a/src/calibre/gui2/preferences/coloring.py
+++ b/src/calibre/gui2/preferences/coloring.py
@@ -192,6 +192,8 @@ class ConditionEditor(QWidget): # {{{
action = self.current_action
if not action:
return
+ m = self.fm[col]
+ dt = m['datatype']
tt = ''
if col == 'identifiers':
tt = _('Enter either an identifier type or an '
@@ -209,7 +211,7 @@ class ConditionEditor(QWidget): # {{{
tt = _('Enter a regular expression')
elif m.get('is_multiple', False):
tt += '\n' + _('You can match multiple values by separating'
- ' them with %s')%m['is_multiple']
+ ' them with %s')%m['is_multiple']['ui_to_list']
self.value_box.setToolTip(tt)
if action in ('is set', 'is not set', 'is true', 'is false',
'is undefined'):
diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py
index 8eaa2dd7d9..d2f1786ab0 100644
--- a/src/calibre/gui2/preferences/create_custom_column.py
+++ b/src/calibre/gui2/preferences/create_custom_column.py
@@ -13,6 +13,9 @@ from calibre.gui2 import error_dialog
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
+ # Note: in this class, we are treating is_multiple as the boolean that
+ # custom_columns expects to find in its structure. It does not use the dict
+
column_types = {
0:{'datatype':'text',
'text':_('Text, column shown in the tag browser'),
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index 470bbcdfa8..601071a2ce 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -509,7 +509,8 @@ class ResultCache(SearchQueryParser): # {{{
valq_mkind, valq = self._matchkind(query)
loc = self.field_metadata[location]['rec_index']
- split_char = self.field_metadata[location]['is_multiple']
+ split_char = self.field_metadata[location]['is_multiple'].get(
+ 'cache_to_list', ',')
for id_ in candidates:
item = self._data[id_]
if item is None:
@@ -665,7 +666,8 @@ class ResultCache(SearchQueryParser): # {{{
if fm['is_multiple'] and \
len(query) > 1 and query.startswith('#') and \
query[1:1] in '=<>!':
- vf = lambda item, loc=fm['rec_index'], ms=fm['is_multiple']:\
+ vf = lambda item, loc=fm['rec_index'], \
+ ms=fm['is_multiple']['cache_to_list']:\
len(item[loc].split(ms)) if item[loc] is not None else 0
return self.get_numeric_matches(location, query[1:],
candidates, val_func=vf)
@@ -703,7 +705,8 @@ class ResultCache(SearchQueryParser): # {{{
['composite', 'text', 'comments', 'series', 'enumeration']:
exclude_fields.append(db_col[x])
col_datatype[db_col[x]] = self.field_metadata[x]['datatype']
- is_multiple_cols[db_col[x]] = self.field_metadata[x]['is_multiple']
+ is_multiple_cols[db_col[x]] = \
+ self.field_metadata[x]['is_multiple'].get('cache_to_list', None)
try:
rating_query = int(query) * 2
@@ -1045,13 +1048,14 @@ class SortKeyGenerator(object):
elif dt in ('text', 'comments', 'composite', 'enumeration'):
if val:
- sep = fm['is_multiple']
- if sep:
- if fm['display'].get('is_names', False):
- val = sep.join(
- [author_to_author_sort(v) for v in val.split(sep)])
+ if fm['is_multiple']:
+ jv = fm['is_multiple']['list_to_ui']
+ sv = fm['is_multiple']['cache_to_list']
+ if '&' in jv:
+ val = jv.join(
+ [author_to_author_sort(v) for v in val.split(sv)])
else:
- val = sep.join(sorted(val.split(sep),
+ val = jv.join(sorted(val.split(sv),
key=self.string_sort_key))
val = self.string_sort_key(val)
diff --git a/src/calibre/library/coloring.py b/src/calibre/library/coloring.py
index f458b9c04f..584cb01e54 100644
--- a/src/calibre/library/coloring.py
+++ b/src/calibre/library/coloring.py
@@ -79,16 +79,19 @@ class Rule(object): # {{{
if dt == 'bool':
return self.bool_condition(col, action, val)
- if dt in ('int', 'float', 'rating'):
+ if dt in ('int', 'float'):
return self.number_condition(col, action, val)
+ if dt == 'rating':
+ return self.rating_condition(col, action, val)
+
if dt == 'datetime':
return self.date_condition(col, action, val)
if dt in ('comments', 'series', 'text', 'enumeration', 'composite'):
ism = m.get('is_multiple', False)
if ism:
- return self.multiple_condition(col, action, val, ',' if ism == '|' else ism)
+ return self.multiple_condition(col, action, val, ism['ui_to_list'])
return self.text_condition(col, action, val)
def identifiers_condition(self, col, action, val):
@@ -114,9 +117,16 @@ class Rule(object): # {{{
'lt': ('1', '', ''),
'gt': ('', '', '1')
}[action]
- lt, eq, gt = '', '1', ''
return "cmp(raw_field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
+ def rating_condition(self, col, action, val):
+ lt, eq, gt = {
+ 'eq': ('', '1', ''),
+ 'lt': ('1', '', ''),
+ 'gt': ('', '', '1')
+ }[action]
+ return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
+
def date_condition(self, col, action, val):
lt, eq, gt = {
'eq': ('', '1', ''),
diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py
index 187d718a39..00ecccc78e 100644
--- a/src/calibre/library/custom_columns.py
+++ b/src/calibre/library/custom_columns.py
@@ -78,6 +78,18 @@ class CustomColumns(object):
}
if data['display'] is None:
data['display'] = {}
+ # set up the is_multiple separator dict
+ if data['is_multiple']:
+ if data['display'].get('is_names', False):
+ seps = {'cache_to_list': '|', 'ui_to_list': '&', 'list_to_ui': ' & '}
+ elif data['datatype'] == 'composite':
+ seps = {'cache_to_list': ',', 'ui_to_list': ',', 'list_to_ui': ', '}
+ else:
+ seps = {'cache_to_list': '|', 'ui_to_list': ',', 'list_to_ui': ', '}
+ else:
+ seps = {}
+ data['multiple_seps'] = seps
+
table, lt = self.custom_table_names(data['num'])
if table not in custom_tables or (data['normalized'] and lt not in
custom_tables):
@@ -119,7 +131,7 @@ class CustomColumns(object):
if x is None:
return []
if isinstance(x, (str, unicode, bytes)):
- x = x.split('&' if d['display'].get('is_names', False) else',')
+ x = x.split(d['multiple_seps']['ui_to_list'])
x = [y.strip() for y in x if y.strip()]
x = [y.decode(preferred_encoding, 'replace') if not isinstance(y,
unicode) else y for y in x]
@@ -181,10 +193,7 @@ class CustomColumns(object):
is_category = True
else:
is_category = False
- if v['is_multiple']:
- is_m = ',' if v['datatype'] == 'composite' else '|'
- else:
- is_m = None
+ is_m = v['multiple_seps']
tn = 'custom_column_{0}'.format(v['num'])
self.field_metadata.add_custom_field(label=v['label'],
table=tn, column='value', datatype=v['datatype'],
@@ -200,7 +209,7 @@ class CustomColumns(object):
row = self.data._data[idx] if index_is_id else self.data[idx]
ans = row[self.FIELD_MAP[data['num']]]
if data['is_multiple'] and data['datatype'] == 'text':
- ans = ans.split('|') if ans else []
+ ans = ans.split(data['multiple_seps']['cache_to_list']) if ans else []
if data['display'].get('sort_alpha', False):
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
return ans
@@ -571,9 +580,17 @@ class CustomColumns(object):
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)'
+# query = 'group_concat(%s.value, "{0}")'.format(
+# data['multiple_seps']['cache_to_list'])
+# if not display.get('sort_alpha', False):
+ if data['multiple_seps']['cache_to_list'] == '|':
+ query = 'sortconcat_bar(link.id, %s.value)'
+ elif data['multiple_seps']['cache_to_list'] == '&':
+ query = 'sortconcat_amper(link.id, %s.value)'
+ else:
+ prints('WARNING: unknown value in multiple_seps',
+ data['multiple_seps']['cache_to_list'])
+ query = 'sortconcat_bar(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}
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index 3a151166e7..9c4c3eb004 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -1250,7 +1250,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
dex = field['rec_index']
for book in self.data.iterall():
if field['is_multiple']:
- vals = [v.strip() for v in book[dex].split(field['is_multiple'])
+ vals = [v.strip() for v in
+ book[dex].split(field['is_multiple']['cache_to_list'])
if v.strip()]
if id_ in vals:
ans.add(book[0])
@@ -1378,7 +1379,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tcategories[category] = {}
# create a list of category/field_index for the books scan to use.
# This saves iterating through field_metadata for each book
- md.append((category, cat['rec_index'], cat['is_multiple'], False))
+ md.append((category, cat['rec_index'],
+ cat['is_multiple'].get('cache_to_list', None), False))
for category in tb_cats.iterkeys():
cat = tb_cats[category]
@@ -1386,7 +1388,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
cat['display'].get('make_category', False):
tids[category] = {}
tcategories[category] = {}
- md.append((category, cat['rec_index'], cat['is_multiple'],
+ md.append((category, cat['rec_index'],
+ cat['is_multiple'].get('cache_to_list', None),
cat['datatype'] == 'composite'))
#print 'end phase "collection":', time.clock() - last, 'seconds'
#last = time.clock()
diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py
index c884542241..231af23038 100644
--- a/src/calibre/library/field_metadata.py
+++ b/src/calibre/library/field_metadata.py
@@ -50,9 +50,16 @@ class FieldMetadata(dict):
datatype: the type of information in the field. Valid values are listed in
VALID_DATA_TYPES below.
- is_multiple: valid for the text datatype. If None, the field is to be
- treated as a single term. If not None, it contains a string, and the field
- is assumed to contain a list of terms separated by that string
+ is_multiple: valid for the text datatype. If {}, the field is to be
+ treated as a single term. If not None, it contains a dict of the form
+ {'cache_to_list': ',',
+ 'ui_to_list': ',',
+ 'list_to_ui': ', '}
+ where the cache_to_list contains the character used to split the value in
+ the meta2 table, ui_to_list contains the character used to create a list
+ from a value shown in the ui (each resulting value must be strip()ed and
+ empty values removed), and list_to_ui contains the string used in join()
+ to create a displayable string from the list.
kind == field: is a db field.
kind == category: standard tag category that isn't a field. see news.
@@ -97,7 +104,9 @@ class FieldMetadata(dict):
'link_column':'author',
'category_sort':'sort',
'datatype':'text',
- 'is_multiple':',',
+ 'is_multiple':{'cache_to_list': ',',
+ 'ui_to_list': '&',
+ 'list_to_ui': ' & '},
'kind':'field',
'name':_('Authors'),
'search_terms':['authors', 'author'],
@@ -109,7 +118,7 @@ class FieldMetadata(dict):
'link_column':'series',
'category_sort':'(title_sort(name))',
'datatype':'series',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Series'),
'search_terms':['series'],
@@ -119,7 +128,9 @@ class FieldMetadata(dict):
('formats', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':',',
+ 'is_multiple':{'cache_to_list': ',',
+ 'ui_to_list': ',',
+ 'list_to_ui': ', '},
'kind':'field',
'name':_('Formats'),
'search_terms':['formats', 'format'],
@@ -131,7 +142,7 @@ class FieldMetadata(dict):
'link_column':'publisher',
'category_sort':'name',
'datatype':'text',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Publishers'),
'search_terms':['publisher'],
@@ -143,7 +154,7 @@ class FieldMetadata(dict):
'link_column':'rating',
'category_sort':'rating',
'datatype':'rating',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Ratings'),
'search_terms':['rating'],
@@ -154,7 +165,7 @@ class FieldMetadata(dict):
'column':'name',
'category_sort':'name',
'datatype':None,
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'category',
'name':_('News'),
'search_terms':[],
@@ -166,7 +177,9 @@ class FieldMetadata(dict):
'link_column': 'tag',
'category_sort':'name',
'datatype':'text',
- 'is_multiple':',',
+ 'is_multiple':{'cache_to_list': ',',
+ 'ui_to_list': ',',
+ 'list_to_ui': ', '},
'kind':'field',
'name':_('Tags'),
'search_terms':['tags', 'tag'],
@@ -176,7 +189,9 @@ class FieldMetadata(dict):
('identifiers', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':',',
+ 'is_multiple':{'cache_to_list': ',',
+ 'ui_to_list': ',',
+ 'list_to_ui': ', '},
'kind':'field',
'name':_('Identifiers'),
'search_terms':['identifiers', 'identifier', 'isbn'],
@@ -186,7 +201,7 @@ class FieldMetadata(dict):
('author_sort',{'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Author Sort'),
'search_terms':['author_sort'],
@@ -196,7 +211,9 @@ class FieldMetadata(dict):
('au_map', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':',',
+ 'is_multiple':{'cache_to_list': ',',
+ 'ui_to_list': None,
+ 'list_to_ui': None},
'kind':'field',
'name':None,
'search_terms':[],
@@ -206,7 +223,7 @@ class FieldMetadata(dict):
('comments', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Comments'),
'search_terms':['comments', 'comment'],
@@ -216,7 +233,7 @@ class FieldMetadata(dict):
('cover', {'table':None,
'column':None,
'datatype':'int',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':None,
'search_terms':['cover'],
@@ -226,7 +243,7 @@ class FieldMetadata(dict):
('id', {'table':None,
'column':None,
'datatype':'int',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':None,
'search_terms':[],
@@ -236,7 +253,7 @@ class FieldMetadata(dict):
('last_modified', {'table':None,
'column':None,
'datatype':'datetime',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Modified'),
'search_terms':['last_modified'],
@@ -246,7 +263,7 @@ class FieldMetadata(dict):
('ondevice', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('On Device'),
'search_terms':['ondevice'],
@@ -256,7 +273,7 @@ class FieldMetadata(dict):
('path', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Path'),
'search_terms':[],
@@ -266,7 +283,7 @@ class FieldMetadata(dict):
('pubdate', {'table':None,
'column':None,
'datatype':'datetime',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Published'),
'search_terms':['pubdate'],
@@ -276,7 +293,7 @@ class FieldMetadata(dict):
('marked', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name': None,
'search_terms':['marked'],
@@ -286,7 +303,7 @@ class FieldMetadata(dict):
('series_index',{'table':None,
'column':None,
'datatype':'float',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':None,
'search_terms':['series_index'],
@@ -296,7 +313,7 @@ class FieldMetadata(dict):
('sort', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Title Sort'),
'search_terms':['title_sort'],
@@ -306,7 +323,7 @@ class FieldMetadata(dict):
('size', {'table':None,
'column':None,
'datatype':'float',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Size'),
'search_terms':['size'],
@@ -316,7 +333,7 @@ class FieldMetadata(dict):
('timestamp', {'table':None,
'column':None,
'datatype':'datetime',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Date'),
'search_terms':['date'],
@@ -326,7 +343,7 @@ class FieldMetadata(dict):
('title', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':_('Title'),
'search_terms':['title'],
@@ -336,7 +353,7 @@ class FieldMetadata(dict):
('uuid', {'table':None,
'column':None,
'datatype':'text',
- 'is_multiple':None,
+ 'is_multiple':{},
'kind':'field',
'name':None,
'search_terms':[],
@@ -508,7 +525,7 @@ class FieldMetadata(dict):
if datatype == 'series':
key += '_index'
self._tb_cats[key] = {'table':None, 'column':None,
- 'datatype':'float', 'is_multiple':None,
+ 'datatype':'float', 'is_multiple':{},
'kind':'field', 'name':'',
'search_terms':[key], 'label':label+'_index',
'colnum':None, 'display':{},
@@ -560,7 +577,7 @@ class FieldMetadata(dict):
if icu_lower(label) != label:
st.append(icu_lower(label))
self._tb_cats[label] = {'table':None, 'column':None,
- 'datatype':None, 'is_multiple':None,
+ 'datatype':None, 'is_multiple':{},
'kind':'user', 'name':name,
'search_terms':st, 'is_custom':False,
'is_category':True, 'is_csp': False}
@@ -570,7 +587,7 @@ class FieldMetadata(dict):
if label in self._tb_cats:
raise ValueError('Duplicate user field [%s]'%(label))
self._tb_cats[label] = {'table':None, 'column':None,
- 'datatype':None, 'is_multiple':None,
+ 'datatype':None, 'is_multiple':{},
'kind':'search', 'name':name,
'search_terms':[], 'is_custom':False,
'is_category':True, 'is_csp': False}
diff --git a/src/calibre/library/restore.py b/src/calibre/library/restore.py
index e03edd449a..20065309aa 100644
--- a/src/calibre/library/restore.py
+++ b/src/calibre/library/restore.py
@@ -171,7 +171,7 @@ class Restore(Thread):
for x in fields:
if x in cfm:
if x == 'is_multiple':
- args.append(cfm[x] is not None)
+ args.append(bool(cfm[x]))
else:
args.append(cfm[x])
if len(args) == len(fields):
diff --git a/src/calibre/library/server/mobile.py b/src/calibre/library/server/mobile.py
index 1bf9f549bc..ad5ee4af96 100644
--- a/src/calibre/library/server/mobile.py
+++ b/src/calibre/library/server/mobile.py
@@ -231,7 +231,8 @@ class MobileServer(object):
book['size'] = human_readable(book['size'])
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown')
- authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
+ aut_is = CFM['authors']['is_multiple']
+ authors = aut_is['list_to_ui'].join([i.replace('|', ',') for i in aus.split(',')])
book['authors'] = authors
book['series_index'] = fmt_sidx(float(record[FM['series_index']]))
book['series'] = record[FM['series']]
@@ -254,8 +255,10 @@ class MobileServer(object):
continue
if datatype == 'text' and CFM[key]['is_multiple']:
book[key] = concat(name,
- format_tag_string(val, ',',
- no_tag_count=True))
+ format_tag_string(val,
+ CFM[key]['is_multiple']['ui_to_list'],
+ no_tag_count=True,
+ joinval=CFM[key]['is_multiple']['list_to_ui']))
else:
book[key] = concat(name, val)
diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py
index 5f6180e68a..04300ea0e3 100644
--- a/src/calibre/library/server/opds.py
+++ b/src/calibre/library/server/opds.py
@@ -180,9 +180,12 @@ def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS, prefix):
if val:
datatype = CFM[key]['datatype']
if datatype == 'text' and CFM[key]['is_multiple']:
- extra.append('%s: %s
'%(xml(name), xml(format_tag_string(val, ',',
- ignore_max=True,
- no_tag_count=True))))
+ extra.append('%s: %s
'%
+ (xml(name),
+ xml(format_tag_string(val,
+ CFM[key]['is_multiple']['ui_to_list'],
+ ignore_max=True, no_tag_count=True,
+ joinval=CFM[key]['is_multiple']['list_to_ui']))))
elif datatype == 'comments':
extra.append('%s: %s
'%(xml(name), comments_to_html(unicode(val))))
else:
diff --git a/src/calibre/library/server/utils.py b/src/calibre/library/server/utils.py
index e58dd2f19b..53c6cdbd9d 100644
--- a/src/calibre/library/server/utils.py
+++ b/src/calibre/library/server/utils.py
@@ -68,7 +68,7 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
except:
return _strftime(fmt, nowf().timetuple())
-def format_tag_string(tags, sep, ignore_max=False, no_tag_count=False):
+def format_tag_string(tags, sep, ignore_max=False, no_tag_count=False, joinval=', '):
MAX = sys.maxint if ignore_max else tweaks['max_content_server_tags_shown']
if tags:
tlist = [t.strip() for t in tags.split(sep)]
@@ -78,10 +78,10 @@ def format_tag_string(tags, sep, ignore_max=False, no_tag_count=False):
if len(tlist) > MAX:
tlist = tlist[:MAX]+['...']
if no_tag_count:
- return ', '.join(tlist) if tlist else ''
+ return joinval.join(tlist) if tlist else ''
else:
return u'%s:&:%s'%(tweaks['max_content_server_tags_shown'],
- ', '.join(tlist)) if tlist else ''
+ joinval.join(tlist)) if tlist else ''
def quote(s):
if isinstance(s, unicode):
diff --git a/src/calibre/library/server/xml.py b/src/calibre/library/server/xml.py
index 14955dc541..18ddf6bb43 100644
--- a/src/calibre/library/server/xml.py
+++ b/src/calibre/library/server/xml.py
@@ -121,8 +121,12 @@ class XMLServer(object):
name = CFM[key]['name']
custcols.append(k)
if datatype == 'text' and CFM[key]['is_multiple']:
- kwargs[k] = concat('#T#'+name, format_tag_string(val,',',
- ignore_max=True))
+ kwargs[k] = \
+ concat('#T#'+name,
+ format_tag_string(val,
+ CFM[key]['is_multiple']['ui_to_list'],
+ ignore_max=True,
+ joinval=CFM[key]['is_multiple']['list_to_ui']))
else:
kwargs[k] = concat(name, val)
kwargs['custcols'] = ','.join(custcols)
diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py
index 511106fe7b..96874d2c27 100644
--- a/src/calibre/library/sqlite.py
+++ b/src/calibre/library/sqlite.py
@@ -121,9 +121,12 @@ class SortedConcatenate(object):
return None
return self.sep.join(map(self.ans.get, sorted(self.ans.keys())))
-class SafeSortedConcatenate(SortedConcatenate):
+class SortedConcatenateBar(SortedConcatenate):
sep = '|'
+class SortedConcatenateAmper(SortedConcatenate):
+ sep = '&'
+
class IdentifiersConcat(object):
'''String concatenation aggregator for the identifiers map'''
def __init__(self):
@@ -220,7 +223,8 @@ class DBThread(Thread):
self.conn.execute('pragma cache_size=5000')
encoding = self.conn.execute('pragma encoding').fetchone()[0]
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
- self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
+ self.conn.create_aggregate('sortconcat_bar', 2, SortedConcatenateBar)
+ self.conn.create_aggregate('sortconcat_amper', 2, SortedConcatenateAmper)
self.conn.create_aggregate('identifiers_concat', 2, IdentifiersConcat)
load_c_extensions(self.conn)
self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row)
diff --git a/src/calibre/library/sqlite_custom.c b/src/calibre/library/sqlite_custom.c
index dee17c79d4..52f0be4575 100644
--- a/src/calibre/library/sqlite_custom.c
+++ b/src/calibre/library/sqlite_custom.c
@@ -141,6 +141,22 @@ static void sort_concat_finalize2(sqlite3_context *context) {
}
+static void sort_concat_finalize3(sqlite3_context *context) {
+ SortConcatList *list;
+ unsigned char *ans;
+
+ list = (SortConcatList*) sqlite3_aggregate_context(context, sizeof(*list));
+
+ if (list != NULL && list->vals != NULL && list->count > 0) {
+ qsort(list->vals, list->count, sizeof(list->vals[0]), sort_concat_cmp);
+ ans = sort_concat_do_finalize(list, '&');
+ if (ans != NULL) sqlite3_result_text(context, (char*)ans, -1, SQLITE_TRANSIENT);
+ free(ans);
+ sort_concat_free(list);
+ }
+
+}
+
// }}}
// identifiers_concat {{{
@@ -237,7 +253,8 @@ MYEXPORT int sqlite3_extension_init(
sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi){
SQLITE_EXTENSION_INIT2(pApi);
sqlite3_create_function(db, "sortconcat", 2, SQLITE_UTF8, NULL, NULL, sort_concat_step, sort_concat_finalize);
- sqlite3_create_function(db, "sort_concat", 2, SQLITE_UTF8, NULL, NULL, sort_concat_step, sort_concat_finalize2);
+ sqlite3_create_function(db, "sortconcat_bar", 2, SQLITE_UTF8, NULL, NULL, sort_concat_step, sort_concat_finalize2);
+ sqlite3_create_function(db, "sortconcat_amper", 2, SQLITE_UTF8, NULL, NULL, sort_concat_step, sort_concat_finalize3);
sqlite3_create_function(db, "identifiers_concat", 2, SQLITE_UTF8, NULL, NULL, identifiers_concat_step, identifiers_concat_finalize);
return 0;
}
diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py
index 1a8867b44e..4c1cec6462 100644
--- a/src/calibre/utils/formatter_functions.py
+++ b/src/calibre/utils/formatter_functions.py
@@ -727,13 +727,8 @@ class BuiltinNot(BuiltinFormatterFunction):
'returns the empty string. This function works well with test or '
'first_non_empty. You can have as many values as you want.')
- def evaluate(self, formatter, kwargs, mi, locals, *args):
- i = 0
- while i < len(args):
- if args[i]:
- return '1'
- i += 1
- return ''
+ def evaluate(self, formatter, kwargs, mi, locals, val):
+ return '' if val else '1'
class BuiltinMergeLists(BuiltinFormatterFunction):
name = 'merge_lists'