Fix various bugs in column coloring. Fix the NOT template function. Migrate is_multiple to a dict

This commit is contained in:
Kovid Goyal 2011-06-04 11:48:39 -06:00
commit 06de23b162
23 changed files with 227 additions and 116 deletions

View File

@ -204,7 +204,8 @@ class CollectionsBookList(BookList):
elif fm['datatype'] == 'text' and fm['is_multiple']: elif fm['datatype'] == 'text' and fm['is_multiple']:
val = orig_val val = orig_val
elif fm['datatype'] == 'composite' and fm['is_multiple']: 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: else:
val = [val] val = [val]

View File

@ -621,10 +621,7 @@ class Metadata(object):
orig_res = res orig_res = res
datatype = cmeta['datatype'] datatype = cmeta['datatype']
if datatype == 'text' and cmeta['is_multiple']: if datatype == 'text' and cmeta['is_multiple']:
if cmeta['display'].get('is_names', False): res = cmeta['is_multiple']['list_to_ui'].join(res)
res = u' & '.join(res)
else:
res = u', '.join(sorted(res, key=sort_key))
elif datatype == 'series' and series_with_index: elif datatype == 'series' and series_with_index:
if self.get_extra(key) is not None: if self.get_extra(key) is not None:
res = res + \ res = res + \
@ -668,7 +665,7 @@ class Metadata(object):
elif datatype == 'text' and fmeta['is_multiple']: elif datatype == 'text' and fmeta['is_multiple']:
if isinstance(res, dict): if isinstance(res, dict):
res = [k + ':' + v for k,v in res.items()] 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: elif datatype == 'series' and series_with_index:
res = res + ' [%s]'%self.format_series_index() res = res + ' [%s]'%self.format_series_index()
elif datatype == 'datetime': elif datatype == 'datetime':

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
lxml based OPF parser. 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 urllib import unquote
from urlparse import urlparse 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(): for name, fm in all_user_metadata.items():
try: 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 = object_to_unicode(fm)
fm = json.dumps(fm, default=to_json, ensure_ascii=False) fm = json.dumps(fm, default=to_json, ensure_ascii=False)
except: except:
@ -585,6 +593,17 @@ class OPF(object): # {{{
fm = elem.get('content') fm = elem.get('content')
try: try:
fm = json.loads(fm, object_hook=from_json) 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) temp.set_user_metadata(name, fm)
except: except:
prints('Failed to read user metadata:', name) prints('Failed to read user metadata:', name)

View File

@ -226,16 +226,14 @@ class Comments(Base):
class Text(Base): class Text(Base):
def setup_ui(self, parent): def setup_ui(self, parent):
if self.col_metadata['display'].get('is_names', False): self.sep = self.col_metadata['multiple_seps']
self.sep = u' & '
else:
self.sep = u', '
values = self.all_values = list(self.db.all_custom(num=self.col_id)) values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key) values.sort(key=sort_key)
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
w = MultiCompleteLineEdit(parent) w = MultiCompleteLineEdit(parent)
w.set_separator(self.sep.strip()) w.set_separator(self.sep['ui_to_list'])
if self.sep == u' & ': if self.sep['ui_to_list'] == '&':
w.set_space_before_sep(True) w.set_space_before_sep(True)
w.set_add_separator(tweaks['authors_completer_append_separator']) w.set_add_separator(tweaks['authors_completer_append_separator'])
w.update_items_cache(values) w.update_items_cache(values)
@ -269,12 +267,12 @@ class Text(Base):
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
if not val: if not val:
val = [] val = []
self.widgets[1].setText(self.sep.join(val)) self.widgets[1].setText(self.sep['list_to_ui'].join(val))
def getter(self): def getter(self):
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
val = unicode(self.widgets[1].text()).strip() 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: if not ans:
ans = None ans = None
return ans return ans
@ -899,9 +897,10 @@ class BulkText(BulkBase):
if not self.a_c_checkbox.isChecked(): if not self.a_c_checkbox.isChecked():
return return
if self.col_metadata['is_multiple']: if self.col_metadata['is_multiple']:
ism = self.col_metadata['multiple_seps']
if self.col_metadata['display'].get('is_names', False): if self.col_metadata['display'].get('is_names', False):
val = self.gui_val 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) self.db.set_custom_bulk(book_ids, add, num=self.col_id)
else: else:
remove_all, adding, rtext = self.gui_val remove_all, adding, rtext = self.gui_val
@ -911,10 +910,10 @@ class BulkText(BulkBase):
else: else:
txt = rtext txt = rtext
if txt: 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 txt = adding
if txt: 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: else:
add = set() add = set()
self.db.set_custom_bulk_multiple(book_ids, add=add, self.db.set_custom_bulk_multiple(book_ids, add=add,

View File

@ -520,7 +520,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
elif not fm['is_multiple']: elif not fm['is_multiple']:
val = [val] val = [val]
elif fm['datatype'] == 'composite': 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': elif field == 'authors':
val = [v.replace('|', ',') for v in val] val = [v.replace('|', ',') for v in val]
else: else:
@ -655,19 +655,10 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
if self.destination_field_fm['is_multiple']: if self.destination_field_fm['is_multiple']:
if self.comma_separated.isChecked(): if self.comma_separated.isChecked():
if dest == 'authors' or \ splitter = self.destination_field_fm['is_multiple']['ui_to_list']
(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 = [] res = []
for v in val: for v in val:
for x in v.split(splitter): res.extend([x.strip() for x in v.split(splitter) if x.strip()])
if x.strip():
res.append(x.strip())
val = res val = res
else: else:
val = [v.replace(',', '') for v in val] val = [v.replace(',', '') for v in val]

View File

@ -254,6 +254,15 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
self.textbox_changed() self.textbox_changed()
self.rule = (None, '') self.rule = (None, '')
tt = _('Template language tutorial')
self.template_tutorial.setText(
'<a href="http://manual.calibre-ebook.com/template_lang.html">'
'%s</a>'%tt)
tt = _('Template function reference')
self.template_func_reference.setText(
'<a href="http://manual.calibre-ebook.com/template_ref.html">'
'%s</a>'%tt)
def textbox_changed(self): def textbox_changed(self):
cur_text = unicode(self.textbox.toPlainText()) cur_text = unicode(self.textbox.toPlainText())
if self.last_text != cur_text: if self.last_text != cur_text:

View File

@ -125,6 +125,20 @@
<item row="9" column="1"> <item row="9" column="1">
<widget class="QPlainTextEdit" name="source_code"/> <widget class="QPlainTextEdit" name="source_code"/>
</item> </item>
<item row="10" column="1">
<widget class="QLabel" name="template_tutorial">
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QLabel" name="template_func_reference">
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>

View File

@ -608,10 +608,11 @@ class BooksModel(QAbstractTableModel): # {{{
def text_type(r, mult=None, idx=-1): def text_type(r, mult=None, idx=-1):
text = self.db.data[r][idx] text = self.db.data[r][idx]
if text and mult is not None: if text and mult:
if mult: jv = mult['list_to_ui']
return QVariant(u' & '.join(text.split('|'))) sv = mult['cache_to_list']
return QVariant(u', '.join(sorted(text.split('|'),key=sort_key))) return QVariant(jv.join(
sorted([t.strip() for t in text.split(sv)], key=sort_key)))
return QVariant(text) return QVariant(text)
def decorated_text_type(r, idx=-1): def decorated_text_type(r, idx=-1):
@ -665,8 +666,6 @@ class BooksModel(QAbstractTableModel): # {{{
datatype = self.custom_columns[col]['datatype'] datatype = self.custom_columns[col]['datatype']
if datatype in ('text', 'comments', 'composite', 'enumeration'): if datatype in ('text', 'comments', 'composite', 'enumeration'):
mult=self.custom_columns[col]['is_multiple'] 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) self.dc[col] = functools.partial(text_type, idx=idx, mult=mult)
if datatype in ['text', 'composite', 'enumeration'] and not mult: if datatype in ['text', 'composite', 'enumeration'] and not mult:
if self.custom_columns[col]['display'].get('use_decorations', False): if self.custom_columns[col]['display'].get('use_decorations', False):
@ -722,9 +721,9 @@ class BooksModel(QAbstractTableModel): # {{{
if id_ in self.color_cache: if id_ in self.color_cache:
if key in self.color_cache[id_]: if key in self.color_cache[id_]:
return self.color_cache[id_][key] return self.color_cache[id_][key]
if mi is None:
mi = self.db.get_metadata(id_, index_is_id=True)
try: try:
if mi is None:
mi = self.db.get_metadata(id_, index_is_id=True)
color = composite_formatter.safe_format(fmt, mi, '', mi) color = composite_formatter.safe_format(fmt, mi, '', mi)
if color in self.colors: if color in self.colors:
color = QColor(color) color = QColor(color)

View File

@ -192,6 +192,8 @@ class ConditionEditor(QWidget): # {{{
action = self.current_action action = self.current_action
if not action: if not action:
return return
m = self.fm[col]
dt = m['datatype']
tt = '' tt = ''
if col == 'identifiers': if col == 'identifiers':
tt = _('Enter either an identifier type or an ' tt = _('Enter either an identifier type or an '
@ -209,7 +211,7 @@ class ConditionEditor(QWidget): # {{{
tt = _('Enter a regular expression') tt = _('Enter a regular expression')
elif m.get('is_multiple', False): elif m.get('is_multiple', False):
tt += '\n' + _('You can match multiple values by separating' 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) self.value_box.setToolTip(tt)
if action in ('is set', 'is not set', 'is true', 'is false', if action in ('is set', 'is not set', 'is true', 'is false',
'is undefined'): 'is undefined'):

View File

@ -13,6 +13,9 @@ from calibre.gui2 import error_dialog
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): 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 = { column_types = {
0:{'datatype':'text', 0:{'datatype':'text',
'text':_('Text, column shown in the tag browser'), 'text':_('Text, column shown in the tag browser'),

View File

@ -509,7 +509,8 @@ class ResultCache(SearchQueryParser): # {{{
valq_mkind, valq = self._matchkind(query) valq_mkind, valq = self._matchkind(query)
loc = self.field_metadata[location]['rec_index'] 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: for id_ in candidates:
item = self._data[id_] item = self._data[id_]
if item is None: if item is None:
@ -665,7 +666,8 @@ class ResultCache(SearchQueryParser): # {{{
if fm['is_multiple'] and \ if fm['is_multiple'] and \
len(query) > 1 and query.startswith('#') and \ len(query) > 1 and query.startswith('#') and \
query[1:1] in '=<>!': 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 len(item[loc].split(ms)) if item[loc] is not None else 0
return self.get_numeric_matches(location, query[1:], return self.get_numeric_matches(location, query[1:],
candidates, val_func=vf) candidates, val_func=vf)
@ -703,7 +705,8 @@ class ResultCache(SearchQueryParser): # {{{
['composite', 'text', 'comments', 'series', 'enumeration']: ['composite', 'text', 'comments', 'series', 'enumeration']:
exclude_fields.append(db_col[x]) exclude_fields.append(db_col[x])
col_datatype[db_col[x]] = self.field_metadata[x]['datatype'] 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: try:
rating_query = int(query) * 2 rating_query = int(query) * 2
@ -1045,13 +1048,14 @@ class SortKeyGenerator(object):
elif dt in ('text', 'comments', 'composite', 'enumeration'): elif dt in ('text', 'comments', 'composite', 'enumeration'):
if val: if val:
sep = fm['is_multiple'] if fm['is_multiple']:
if sep: jv = fm['is_multiple']['list_to_ui']
if fm['display'].get('is_names', False): sv = fm['is_multiple']['cache_to_list']
val = sep.join( if '&' in jv:
[author_to_author_sort(v) for v in val.split(sep)]) val = jv.join(
[author_to_author_sort(v) for v in val.split(sv)])
else: else:
val = sep.join(sorted(val.split(sep), val = jv.join(sorted(val.split(sv),
key=self.string_sort_key)) key=self.string_sort_key))
val = self.string_sort_key(val) val = self.string_sort_key(val)

View File

@ -79,16 +79,19 @@ class Rule(object): # {{{
if dt == 'bool': if dt == 'bool':
return self.bool_condition(col, action, val) 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) return self.number_condition(col, action, val)
if dt == 'rating':
return self.rating_condition(col, action, val)
if dt == 'datetime': if dt == 'datetime':
return self.date_condition(col, action, val) return self.date_condition(col, action, val)
if dt in ('comments', 'series', 'text', 'enumeration', 'composite'): if dt in ('comments', 'series', 'text', 'enumeration', 'composite'):
ism = m.get('is_multiple', False) ism = m.get('is_multiple', False)
if ism: 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) return self.text_condition(col, action, val)
def identifiers_condition(self, col, action, val): def identifiers_condition(self, col, action, val):
@ -114,9 +117,16 @@ class Rule(object): # {{{
'lt': ('1', '', ''), 'lt': ('1', '', ''),
'gt': ('', '', '1') 'gt': ('', '', '1')
}[action] }[action]
lt, eq, gt = '', '1', ''
return "cmp(raw_field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt) 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): def date_condition(self, col, action, val):
lt, eq, gt = { lt, eq, gt = {
'eq': ('', '1', ''), 'eq': ('', '1', ''),

View File

@ -78,6 +78,18 @@ class CustomColumns(object):
} }
if data['display'] is None: if data['display'] is None:
data['display'] = {} 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']) table, lt = self.custom_table_names(data['num'])
if table not in custom_tables or (data['normalized'] and lt not in if table not in custom_tables or (data['normalized'] and lt not in
custom_tables): custom_tables):
@ -119,7 +131,7 @@ class CustomColumns(object):
if x is None: if x is None:
return [] return []
if isinstance(x, (str, unicode, bytes)): 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.strip() for y in x if y.strip()]
x = [y.decode(preferred_encoding, 'replace') if not isinstance(y, x = [y.decode(preferred_encoding, 'replace') if not isinstance(y,
unicode) else y for y in x] unicode) else y for y in x]
@ -181,10 +193,7 @@ class CustomColumns(object):
is_category = True is_category = True
else: else:
is_category = False is_category = False
if v['is_multiple']: is_m = v['multiple_seps']
is_m = ',' if v['datatype'] == 'composite' else '|'
else:
is_m = None
tn = 'custom_column_{0}'.format(v['num']) tn = 'custom_column_{0}'.format(v['num'])
self.field_metadata.add_custom_field(label=v['label'], self.field_metadata.add_custom_field(label=v['label'],
table=tn, column='value', datatype=v['datatype'], 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] row = self.data._data[idx] if index_is_id else self.data[idx]
ans = row[self.FIELD_MAP[data['num']]] ans = row[self.FIELD_MAP[data['num']]]
if data['is_multiple'] and data['datatype'] == 'text': 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): if data['display'].get('sort_alpha', False):
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower())) ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
return ans return ans
@ -571,9 +580,17 @@ class CustomColumns(object):
if data['normalized']: if data['normalized']:
query = '%s.value' query = '%s.value'
if data['is_multiple']: if data['is_multiple']:
query = 'group_concat(%s.value, "|")' # query = 'group_concat(%s.value, "{0}")'.format(
if not display.get('sort_alpha', False): # data['multiple_seps']['cache_to_list'])
query = 'sort_concat(link.id, %s.value)' # 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 line = '''(SELECT {query} FROM {lt} AS link INNER JOIN
{table} ON(link.value={table}.id) WHERE link.book=books.id) {table} ON(link.value={table}.id) WHERE link.book=books.id)
custom_{num} custom_{num}

View File

@ -1250,7 +1250,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
dex = field['rec_index'] dex = field['rec_index']
for book in self.data.iterall(): for book in self.data.iterall():
if field['is_multiple']: 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 v.strip()]
if id_ in vals: if id_ in vals:
ans.add(book[0]) ans.add(book[0])
@ -1378,7 +1379,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tcategories[category] = {} tcategories[category] = {}
# create a list of category/field_index for the books scan to use. # create a list of category/field_index for the books scan to use.
# This saves iterating through field_metadata for each book # 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(): for category in tb_cats.iterkeys():
cat = tb_cats[category] cat = tb_cats[category]
@ -1386,7 +1388,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
cat['display'].get('make_category', False): cat['display'].get('make_category', False):
tids[category] = {} tids[category] = {}
tcategories[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')) cat['datatype'] == 'composite'))
#print 'end phase "collection":', time.clock() - last, 'seconds' #print 'end phase "collection":', time.clock() - last, 'seconds'
#last = time.clock() #last = time.clock()

View File

@ -50,9 +50,16 @@ class FieldMetadata(dict):
datatype: the type of information in the field. Valid values are listed in datatype: the type of information in the field. Valid values are listed in
VALID_DATA_TYPES below. VALID_DATA_TYPES below.
is_multiple: valid for the text datatype. If None, the field is to be is_multiple: valid for the text datatype. If {}, the field is to be
treated as a single term. If not None, it contains a string, and the field treated as a single term. If not None, it contains a dict of the form
is assumed to contain a list of terms separated by that string {'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 == field: is a db field.
kind == category: standard tag category that isn't a field. see news. kind == category: standard tag category that isn't a field. see news.
@ -97,7 +104,9 @@ class FieldMetadata(dict):
'link_column':'author', 'link_column':'author',
'category_sort':'sort', 'category_sort':'sort',
'datatype':'text', 'datatype':'text',
'is_multiple':',', 'is_multiple':{'cache_to_list': ',',
'ui_to_list': '&',
'list_to_ui': ' & '},
'kind':'field', 'kind':'field',
'name':_('Authors'), 'name':_('Authors'),
'search_terms':['authors', 'author'], 'search_terms':['authors', 'author'],
@ -109,7 +118,7 @@ class FieldMetadata(dict):
'link_column':'series', 'link_column':'series',
'category_sort':'(title_sort(name))', 'category_sort':'(title_sort(name))',
'datatype':'series', 'datatype':'series',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Series'), 'name':_('Series'),
'search_terms':['series'], 'search_terms':['series'],
@ -119,7 +128,9 @@ class FieldMetadata(dict):
('formats', {'table':None, ('formats', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':',', 'is_multiple':{'cache_to_list': ',',
'ui_to_list': ',',
'list_to_ui': ', '},
'kind':'field', 'kind':'field',
'name':_('Formats'), 'name':_('Formats'),
'search_terms':['formats', 'format'], 'search_terms':['formats', 'format'],
@ -131,7 +142,7 @@ class FieldMetadata(dict):
'link_column':'publisher', 'link_column':'publisher',
'category_sort':'name', 'category_sort':'name',
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Publishers'), 'name':_('Publishers'),
'search_terms':['publisher'], 'search_terms':['publisher'],
@ -143,7 +154,7 @@ class FieldMetadata(dict):
'link_column':'rating', 'link_column':'rating',
'category_sort':'rating', 'category_sort':'rating',
'datatype':'rating', 'datatype':'rating',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Ratings'), 'name':_('Ratings'),
'search_terms':['rating'], 'search_terms':['rating'],
@ -154,7 +165,7 @@ class FieldMetadata(dict):
'column':'name', 'column':'name',
'category_sort':'name', 'category_sort':'name',
'datatype':None, 'datatype':None,
'is_multiple':None, 'is_multiple':{},
'kind':'category', 'kind':'category',
'name':_('News'), 'name':_('News'),
'search_terms':[], 'search_terms':[],
@ -166,7 +177,9 @@ class FieldMetadata(dict):
'link_column': 'tag', 'link_column': 'tag',
'category_sort':'name', 'category_sort':'name',
'datatype':'text', 'datatype':'text',
'is_multiple':',', 'is_multiple':{'cache_to_list': ',',
'ui_to_list': ',',
'list_to_ui': ', '},
'kind':'field', 'kind':'field',
'name':_('Tags'), 'name':_('Tags'),
'search_terms':['tags', 'tag'], 'search_terms':['tags', 'tag'],
@ -176,7 +189,9 @@ class FieldMetadata(dict):
('identifiers', {'table':None, ('identifiers', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':',', 'is_multiple':{'cache_to_list': ',',
'ui_to_list': ',',
'list_to_ui': ', '},
'kind':'field', 'kind':'field',
'name':_('Identifiers'), 'name':_('Identifiers'),
'search_terms':['identifiers', 'identifier', 'isbn'], 'search_terms':['identifiers', 'identifier', 'isbn'],
@ -186,7 +201,7 @@ class FieldMetadata(dict):
('author_sort',{'table':None, ('author_sort',{'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Author Sort'), 'name':_('Author Sort'),
'search_terms':['author_sort'], 'search_terms':['author_sort'],
@ -196,7 +211,9 @@ class FieldMetadata(dict):
('au_map', {'table':None, ('au_map', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':',', 'is_multiple':{'cache_to_list': ',',
'ui_to_list': None,
'list_to_ui': None},
'kind':'field', 'kind':'field',
'name':None, 'name':None,
'search_terms':[], 'search_terms':[],
@ -206,7 +223,7 @@ class FieldMetadata(dict):
('comments', {'table':None, ('comments', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Comments'), 'name':_('Comments'),
'search_terms':['comments', 'comment'], 'search_terms':['comments', 'comment'],
@ -216,7 +233,7 @@ class FieldMetadata(dict):
('cover', {'table':None, ('cover', {'table':None,
'column':None, 'column':None,
'datatype':'int', 'datatype':'int',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':None, 'name':None,
'search_terms':['cover'], 'search_terms':['cover'],
@ -226,7 +243,7 @@ class FieldMetadata(dict):
('id', {'table':None, ('id', {'table':None,
'column':None, 'column':None,
'datatype':'int', 'datatype':'int',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':None, 'name':None,
'search_terms':[], 'search_terms':[],
@ -236,7 +253,7 @@ class FieldMetadata(dict):
('last_modified', {'table':None, ('last_modified', {'table':None,
'column':None, 'column':None,
'datatype':'datetime', 'datatype':'datetime',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Modified'), 'name':_('Modified'),
'search_terms':['last_modified'], 'search_terms':['last_modified'],
@ -246,7 +263,7 @@ class FieldMetadata(dict):
('ondevice', {'table':None, ('ondevice', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('On Device'), 'name':_('On Device'),
'search_terms':['ondevice'], 'search_terms':['ondevice'],
@ -256,7 +273,7 @@ class FieldMetadata(dict):
('path', {'table':None, ('path', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Path'), 'name':_('Path'),
'search_terms':[], 'search_terms':[],
@ -266,7 +283,7 @@ class FieldMetadata(dict):
('pubdate', {'table':None, ('pubdate', {'table':None,
'column':None, 'column':None,
'datatype':'datetime', 'datatype':'datetime',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Published'), 'name':_('Published'),
'search_terms':['pubdate'], 'search_terms':['pubdate'],
@ -276,7 +293,7 @@ class FieldMetadata(dict):
('marked', {'table':None, ('marked', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name': None, 'name': None,
'search_terms':['marked'], 'search_terms':['marked'],
@ -286,7 +303,7 @@ class FieldMetadata(dict):
('series_index',{'table':None, ('series_index',{'table':None,
'column':None, 'column':None,
'datatype':'float', 'datatype':'float',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':None, 'name':None,
'search_terms':['series_index'], 'search_terms':['series_index'],
@ -296,7 +313,7 @@ class FieldMetadata(dict):
('sort', {'table':None, ('sort', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Title Sort'), 'name':_('Title Sort'),
'search_terms':['title_sort'], 'search_terms':['title_sort'],
@ -306,7 +323,7 @@ class FieldMetadata(dict):
('size', {'table':None, ('size', {'table':None,
'column':None, 'column':None,
'datatype':'float', 'datatype':'float',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Size'), 'name':_('Size'),
'search_terms':['size'], 'search_terms':['size'],
@ -316,7 +333,7 @@ class FieldMetadata(dict):
('timestamp', {'table':None, ('timestamp', {'table':None,
'column':None, 'column':None,
'datatype':'datetime', 'datatype':'datetime',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Date'), 'name':_('Date'),
'search_terms':['date'], 'search_terms':['date'],
@ -326,7 +343,7 @@ class FieldMetadata(dict):
('title', {'table':None, ('title', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':_('Title'), 'name':_('Title'),
'search_terms':['title'], 'search_terms':['title'],
@ -336,7 +353,7 @@ class FieldMetadata(dict):
('uuid', {'table':None, ('uuid', {'table':None,
'column':None, 'column':None,
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':{},
'kind':'field', 'kind':'field',
'name':None, 'name':None,
'search_terms':[], 'search_terms':[],
@ -508,7 +525,7 @@ class FieldMetadata(dict):
if datatype == 'series': if datatype == 'series':
key += '_index' key += '_index'
self._tb_cats[key] = {'table':None, 'column':None, self._tb_cats[key] = {'table':None, 'column':None,
'datatype':'float', 'is_multiple':None, 'datatype':'float', 'is_multiple':{},
'kind':'field', 'name':'', 'kind':'field', 'name':'',
'search_terms':[key], 'label':label+'_index', 'search_terms':[key], 'label':label+'_index',
'colnum':None, 'display':{}, 'colnum':None, 'display':{},
@ -560,7 +577,7 @@ class FieldMetadata(dict):
if icu_lower(label) != label: if icu_lower(label) != label:
st.append(icu_lower(label)) st.append(icu_lower(label))
self._tb_cats[label] = {'table':None, 'column':None, self._tb_cats[label] = {'table':None, 'column':None,
'datatype':None, 'is_multiple':None, 'datatype':None, 'is_multiple':{},
'kind':'user', 'name':name, 'kind':'user', 'name':name,
'search_terms':st, 'is_custom':False, 'search_terms':st, 'is_custom':False,
'is_category':True, 'is_csp': False} 'is_category':True, 'is_csp': False}
@ -570,7 +587,7 @@ class FieldMetadata(dict):
if label in self._tb_cats: if label in self._tb_cats:
raise ValueError('Duplicate user field [%s]'%(label)) raise ValueError('Duplicate user field [%s]'%(label))
self._tb_cats[label] = {'table':None, 'column':None, self._tb_cats[label] = {'table':None, 'column':None,
'datatype':None, 'is_multiple':None, 'datatype':None, 'is_multiple':{},
'kind':'search', 'name':name, 'kind':'search', 'name':name,
'search_terms':[], 'is_custom':False, 'search_terms':[], 'is_custom':False,
'is_category':True, 'is_csp': False} 'is_category':True, 'is_csp': False}

View File

@ -171,7 +171,7 @@ class Restore(Thread):
for x in fields: for x in fields:
if x in cfm: if x in cfm:
if x == 'is_multiple': if x == 'is_multiple':
args.append(cfm[x] is not None) args.append(bool(cfm[x]))
else: else:
args.append(cfm[x]) args.append(cfm[x])
if len(args) == len(fields): if len(args) == len(fields):

View File

@ -231,7 +231,8 @@ class MobileServer(object):
book['size'] = human_readable(book['size']) book['size'] = human_readable(book['size'])
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown') 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['authors'] = authors
book['series_index'] = fmt_sidx(float(record[FM['series_index']])) book['series_index'] = fmt_sidx(float(record[FM['series_index']]))
book['series'] = record[FM['series']] book['series'] = record[FM['series']]
@ -254,8 +255,10 @@ class MobileServer(object):
continue continue
if datatype == 'text' and CFM[key]['is_multiple']: if datatype == 'text' and CFM[key]['is_multiple']:
book[key] = concat(name, book[key] = concat(name,
format_tag_string(val, ',', format_tag_string(val,
no_tag_count=True)) CFM[key]['is_multiple']['ui_to_list'],
no_tag_count=True,
joinval=CFM[key]['is_multiple']['list_to_ui']))
else: else:
book[key] = concat(name, val) book[key] = concat(name, val)

View File

@ -180,9 +180,12 @@ def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS, prefix):
if val: if val:
datatype = CFM[key]['datatype'] datatype = CFM[key]['datatype']
if datatype == 'text' and CFM[key]['is_multiple']: if datatype == 'text' and CFM[key]['is_multiple']:
extra.append('%s: %s<br />'%(xml(name), xml(format_tag_string(val, ',', extra.append('%s: %s<br />'%
ignore_max=True, (xml(name),
no_tag_count=True)))) 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': elif datatype == 'comments':
extra.append('%s: %s<br />'%(xml(name), comments_to_html(unicode(val)))) extra.append('%s: %s<br />'%(xml(name), comments_to_html(unicode(val))))
else: else:

View File

@ -68,7 +68,7 @@ def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
except: except:
return _strftime(fmt, nowf().timetuple()) 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'] MAX = sys.maxint if ignore_max else tweaks['max_content_server_tags_shown']
if tags: if tags:
tlist = [t.strip() for t in tags.split(sep)] 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: if len(tlist) > MAX:
tlist = tlist[:MAX]+['...'] tlist = tlist[:MAX]+['...']
if no_tag_count: if no_tag_count:
return ', '.join(tlist) if tlist else '' return joinval.join(tlist) if tlist else ''
else: else:
return u'%s:&:%s'%(tweaks['max_content_server_tags_shown'], return u'%s:&:%s'%(tweaks['max_content_server_tags_shown'],
', '.join(tlist)) if tlist else '' joinval.join(tlist)) if tlist else ''
def quote(s): def quote(s):
if isinstance(s, unicode): if isinstance(s, unicode):

View File

@ -121,8 +121,12 @@ class XMLServer(object):
name = CFM[key]['name'] name = CFM[key]['name']
custcols.append(k) custcols.append(k)
if datatype == 'text' and CFM[key]['is_multiple']: if datatype == 'text' and CFM[key]['is_multiple']:
kwargs[k] = concat('#T#'+name, format_tag_string(val,',', kwargs[k] = \
ignore_max=True)) 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: else:
kwargs[k] = concat(name, val) kwargs[k] = concat(name, val)
kwargs['custcols'] = ','.join(custcols) kwargs['custcols'] = ','.join(custcols)

View File

@ -121,9 +121,12 @@ class SortedConcatenate(object):
return None return None
return self.sep.join(map(self.ans.get, sorted(self.ans.keys()))) return self.sep.join(map(self.ans.get, sorted(self.ans.keys())))
class SafeSortedConcatenate(SortedConcatenate): class SortedConcatenateBar(SortedConcatenate):
sep = '|' sep = '|'
class SortedConcatenateAmper(SortedConcatenate):
sep = '&'
class IdentifiersConcat(object): class IdentifiersConcat(object):
'''String concatenation aggregator for the identifiers map''' '''String concatenation aggregator for the identifiers map'''
def __init__(self): def __init__(self):
@ -220,7 +223,8 @@ class DBThread(Thread):
self.conn.execute('pragma cache_size=5000') self.conn.execute('pragma cache_size=5000')
encoding = self.conn.execute('pragma encoding').fetchone()[0] encoding = self.conn.execute('pragma encoding').fetchone()[0]
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) 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) self.conn.create_aggregate('identifiers_concat', 2, IdentifiersConcat)
load_c_extensions(self.conn) load_c_extensions(self.conn)
self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row) self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row)

View File

@ -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 {{{ // identifiers_concat {{{
@ -237,7 +253,8 @@ MYEXPORT int sqlite3_extension_init(
sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi){ sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi){
SQLITE_EXTENSION_INIT2(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, "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); sqlite3_create_function(db, "identifiers_concat", 2, SQLITE_UTF8, NULL, NULL, identifiers_concat_step, identifiers_concat_finalize);
return 0; return 0;
} }

View File

@ -727,13 +727,8 @@ class BuiltinNot(BuiltinFormatterFunction):
'returns the empty string. This function works well with test or ' 'returns the empty string. This function works well with test or '
'first_non_empty. You can have as many values as you want.') 'first_non_empty. You can have as many values as you want.')
def evaluate(self, formatter, kwargs, mi, locals, *args): def evaluate(self, formatter, kwargs, mi, locals, val):
i = 0 return '' if val else '1'
while i < len(args):
if args[i]:
return '1'
i += 1
return ''
class BuiltinMergeLists(BuiltinFormatterFunction): class BuiltinMergeLists(BuiltinFormatterFunction):
name = 'merge_lists' name = 'merge_lists'