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'