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/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..a6f7ed8843 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,14 +655,14 @@ 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'] +# 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 = ',' res = [] for v in val: for x in v.split(splitter): 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 695adabed8..793d529c92 100644 --- a/src/calibre/gui2/preferences/coloring.py +++ b/src/calibre/gui2/preferences/coloring.py @@ -205,7 +205,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..17bec7e0e8 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -509,7 +509,7 @@ 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']['cache_to_list'] for id_ in candidates: item = self._data[id_] if item is None: @@ -665,7 +665,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 +704,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 +1047,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..0009b4636a 100644 --- a/src/calibre/library/coloring.py +++ b/src/calibre/library/coloring.py @@ -88,7 +88,7 @@ class Rule(object): # {{{ 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): 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/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)