1) add the composite field custom datatype

2) clean up content server code so it uses the new formatting facilities
This commit is contained in:
Charles Haley 2010-09-18 11:08:33 +01:00
parent 69d021a542
commit 7881286274
13 changed files with 181 additions and 96 deletions

View File

@ -137,7 +137,6 @@ class CollectionsBookList(BookList):
# For existing books, modify the collections only if the user
# specified 'on_connect'
attrs = collection_attributes
meta_vals = book.get_all_non_none_attributes()
for attr in attrs:
attr = attr.strip()
ign, val, orig_val, fm = book.format_field_extended(attr)
@ -166,7 +165,7 @@ class CollectionsBookList(BookList):
continue
if attr == 'series' or \
('series' in collection_attributes and
meta_vals.get('series', None) == category):
book.get('series', None) == category):
is_series = True
cat_name = self.compute_category_name(attr, category, fm)
if cat_name not in collections:
@ -177,10 +176,10 @@ class CollectionsBookList(BookList):
collections_lpaths[cat_name].add(lpath)
if is_series:
collections[cat_name].append(
(book, meta_vals.get(attr+'_index', sys.maxint)))
(book, book.get(attr+'_index', sys.maxint)))
else:
collections[cat_name].append(
(book, meta_vals.get('title_sort', 'zzzz')))
(book, book.get('title_sort', 'zzzz')))
# Sort collections
result = {}
for category, books in collections.items():

View File

@ -81,9 +81,8 @@ DEVICE_METADATA_FIELDS = frozenset([
CALIBRE_METADATA_FIELDS = frozenset([
'application_id', # An application id, currently set to the db_id.
# the calibre primary key of the item.
'db_id', # the calibre primary key of the item.
# TODO: NEWMETA: May want to remove once Sony's no longer use it
'formats', # list of formats (extensions) for this book
]
)
@ -124,5 +123,5 @@ SERIALIZABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS).union(
DEVICE_METADATA_FIELDS) - \
frozenset(['device_collections'])
# device_collections is rebuilt when needed
frozenset(['device_collections', 'formats'])
# these are rebuilt when needed

View File

@ -5,8 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import copy
import traceback
import copy, re, string, traceback
from calibre import prints
from calibre.ebooks.metadata.book import SC_COPYABLE_FIELDS
@ -33,6 +32,23 @@ NULL_VALUES = {
field_metadata = FieldMetadata()
class SafeFormat(string.Formatter):
'''
Provides a format function that substitutes '' for any missing value
'''
def get_value(self, key, args, mi):
ign, v = mi.format_field(key, series_with_index=False)
if v is None:
return ''
return v
composite_formatter = SafeFormat()
compress_spaces = re.compile(r'\s+')
def format_composite(x, mi):
ans = composite_formatter.vformat(x, [], mi).strip()
return compress_spaces.sub(' ', ans)
class Metadata(object):
'''
@ -343,18 +359,19 @@ class Metadata(object):
def format_rating(self):
return unicode(self.rating)
def format_field(self, key):
name, val, ign, ign = self.format_field_extended(key)
def format_field(self, key, series_with_index=True):
name, val, ign, ign = self.format_field_extended(key, series_with_index)
return (name, val)
def format_field_extended(self, key):
def format_field_extended(self, key, series_with_index=True):
from calibre.ebooks.metadata import authors_to_string
'''
returns the tuple (field_name, formatted_value)
'''
if key in self.user_metadata_keys:
res = self.get(key, None)
if res is None or res == '':
cmeta = self.get_user_metadata(key, make_copy=False)
if cmeta['datatype'] != 'composite' and (res is None or res == ''):
return (None, None, None, None)
orig_res = res
cmeta = self.get_user_metadata(key, make_copy=False)
@ -362,13 +379,15 @@ class Metadata(object):
datatype = cmeta['datatype']
if datatype == 'text' and cmeta['is_multiple']:
res = u', '.join(res)
elif datatype == 'series':
elif datatype == 'series' and series_with_index:
res = res + \
' [%s]'%self.format_series_index(val=self.get_extra(key))
elif datatype == 'datetime':
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool':
res = _('Yes') if res else _('No')
elif datatype == 'composite':
res = format_composite(cmeta['display']['composite_template'], self)
return (name, res, orig_res, cmeta)
if key in field_metadata and field_metadata[key]['kind'] == 'field':
@ -383,7 +402,7 @@ class Metadata(object):
res = authors_to_string(res)
elif datatype == 'text' and fmeta['is_multiple']:
res = u', '.join(res)
elif datatype == 'series':
elif datatype == 'series' and series_with_index:
res = res + ' [%s]'%self.format_series_index()
elif datatype == 'datetime':
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))

View File

@ -86,6 +86,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.last_search = '' # The last search performed on this model
self.column_map = []
self.headers = {}
self.metadata_cache = {}
self.alignment_map = {}
self.buffer_size = buffer
self.cover_cache = None
@ -114,6 +115,16 @@ class BooksModel(QAbstractTableModel): # {{{
def clear_caches(self):
if self.cover_cache:
self.cover_cache.clear_cache()
self.metadata_cache = {}
def get_cached_metadata(self, idx):
if idx not in self.metadata_cache:
self.metadata_cache[idx] = self.db.get_metadata(idx)
return self.metadata_cache[idx]
def remove_cached_metadata(self, idx):
if idx in self.metadata_cache:
del self.metadata_cache[idx]
def read_config(self):
self.use_roman_numbers = config['use_roman_numerals_for_series_number']
@ -146,6 +157,7 @@ class BooksModel(QAbstractTableModel): # {{{
elif col in self.custom_columns:
self.headers[col] = self.custom_columns[col]['name']
self.metadata_cache = {}
self.build_data_convertors()
self.reset()
self.database_changed.emit(db)
@ -159,11 +171,13 @@ class BooksModel(QAbstractTableModel): # {{{
db.add_listener(refresh_cover)
def refresh_ids(self, ids, current_row=-1):
self.metadata_cache = {}
rows = self.db.refresh_ids(ids)
if rows:
self.refresh_rows(rows, current_row=current_row)
def refresh_rows(self, rows, current_row=-1):
self.metadata_cache = {}
for row in rows:
if row == current_row:
self.new_bookdisplay_data.emit(
@ -193,6 +207,7 @@ class BooksModel(QAbstractTableModel): # {{{
return ret
def count_changed(self, *args):
self.metadata_cache = {}
self.count_changed_signal.emit(self.db.count())
def row_indices(self, index):
@ -262,6 +277,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.sorting_done.emit(self.db.index)
def refresh(self, reset=True):
self.metadata_cache = {}
self.db.refresh(field=None)
self.resort(reset=reset)
@ -318,7 +334,7 @@ class BooksModel(QAbstractTableModel): # {{{
data[_('Series')] = \
_('Book <font face="serif">%s</font> of %s.')%\
(sidx, prepare_string_for_xml(series))
mi = self.db.get_metadata(idx)
mi = self.get_cached_metadata(idx)
for key in mi.user_metadata_keys:
name, val = mi.format_field(key)
if val is not None:
@ -327,6 +343,7 @@ class BooksModel(QAbstractTableModel): # {{{
def set_cache(self, idx):
l, r = 0, self.count()-1
self.remove_cached_metadata(idx)
if self.cover_cache is not None:
l = max(l, idx-self.buffer_size)
r = min(r, idx+self.buffer_size)
@ -586,6 +603,10 @@ class BooksModel(QAbstractTableModel): # {{{
def number_type(r, idx=-1):
return QVariant(self.db.data[r][idx])
def composite_type(r, key=None):
mi = self.get_cached_metadata(r)
return QVariant(mi.format_field(key)[1])
self.dc = {
'title' : functools.partial(text_type,
idx=self.db.field_metadata['title']['rec_index'], mult=False),
@ -620,7 +641,8 @@ class BooksModel(QAbstractTableModel): # {{{
idx = self.custom_columns[col]['rec_index']
datatype = self.custom_columns[col]['datatype']
if datatype in ('text', 'comments'):
self.dc[col] = functools.partial(text_type, idx=idx, mult=self.custom_columns[col]['is_multiple'])
self.dc[col] = functools.partial(text_type, idx=idx,
mult=self.custom_columns[col]['is_multiple'])
elif datatype in ('int', 'float'):
self.dc[col] = functools.partial(number_type, idx=idx)
elif datatype == 'datetime':
@ -628,13 +650,15 @@ class BooksModel(QAbstractTableModel): # {{{
elif datatype == 'bool':
self.dc[col] = functools.partial(bool_type, idx=idx)
self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx,
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes')
bool_type_decorator, idx=idx,
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes')
elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx)
elif datatype == 'series':
self.dc[col] = functools.partial(series_type, idx=idx,
siix=self.db.field_metadata.cc_series_index_column_for(col))
elif datatype == 'composite':
self.dc[col] = functools.partial(composite_type, key=col)
else:
print 'What type is this?', col, datatype
# build a index column to data converter map, to remove the string lookup in the data loop
@ -729,6 +753,7 @@ class BooksModel(QAbstractTableModel): # {{{
if role == Qt.EditRole:
row, col = index.row(), index.column()
column = self.column_map[col]
self.remove_cached_metadata(row)
if self.is_custom_column(column):
if not self.set_custom_column_data(row, column, value):
return False

View File

@ -155,7 +155,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
name=self.custcols[c]['name'],
datatype=self.custcols[c]['datatype'],
is_multiple=self.custcols[c]['is_multiple'],
display = self.custcols[c]['display'])
display = self.custcols[c]['display'],
editable = self.custcols[c]['editable'])
must_restart = True
elif '*deleteme' in self.custcols[c]:
db.delete_custom_column(label=self.custcols[c]['label'])

View File

@ -38,6 +38,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
'is_multiple':False},
8:{'datatype':'bool',
'text':_('Yes/No'), 'is_multiple':False},
8:{'datatype':'composite',
'text':_('Field built from other fields'), 'is_multiple':False},
}
def __init__(self, parent, editing, standard_colheads, standard_colnames):
@ -86,6 +88,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
if ct == 'datetime':
if c['display'].get('date_format', None):
self.date_format_box.setText(c['display'].get('date_format', ''))
elif ct == 'composite':
self.composite_box.setText(c['display'].get('composite_template', ''))
self.datatype_changed()
self.exec_()
@ -94,9 +98,10 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
except:
col_type = None
df_visible = col_type == 'datetime'
for x in ('box', 'default_label', 'label'):
getattr(self, 'date_format_'+x).setVisible(df_visible)
getattr(self, 'date_format_'+x).setVisible(col_type == 'datetime')
for x in ('box', 'default_label', 'label'):
getattr(self, 'composite_'+x).setVisible(col_type == 'composite')
def accept(self):
@ -122,6 +127,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
bad_col = True
if bad_col:
return self.simple_error('', _('The lookup name %s is already used')%col)
bad_head = False
for t in self.parent.custcols:
if self.parent.custcols[t]['name'] == col_heading:
@ -133,12 +139,20 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
if bad_head:
return self.simple_error('', _('The heading %s is already used')%col_heading)
date_format = {}
display_dict = {}
if col_type == 'datetime':
if self.date_format_box.text():
date_format = {'date_format':unicode(self.date_format_box.text())}
display_dict = {'date_format':unicode(self.date_format_box.text())}
else:
date_format = {'date_format': None}
display_dict = {'date_format': None}
if col_type == 'composite':
if not self.composite_box.text():
return self.simple_error('', _('You must enter a template for composite fields')%col_heading)
display_dict = {'composite_template':unicode(self.composite_box.text())}
is_editable = False
else:
is_editable = True
db = self.parent.gui.library_view.model().db
key = db.field_metadata.custom_field_prefix+col
@ -148,8 +162,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
'label':col,
'name':col_heading,
'datatype':col_type,
'editable':True,
'display':date_format,
'editable':is_editable,
'display':display_dict,
'normalized':None,
'colnum':None,
'is_multiple':is_multiple,
@ -164,7 +178,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
item.setText(col_heading)
self.parent.custcols[self.orig_column_name]['label'] = col
self.parent.custcols[self.orig_column_name]['name'] = col_heading
self.parent.custcols[self.orig_column_name]['display'].update(date_format)
self.parent.custcols[self.orig_column_name]['display'].update(display_dict)
self.parent.custcols[self.orig_column_name]['*edited'] = True
self.parent.custcols[self.orig_column_name]['*must_restart'] = True
QDialog.accept(self)

View File

@ -147,9 +147,59 @@
</property>
</widget>
</item>
<item row="5" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLineEdit" name="composite_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;p&gt;Field template. Uses the same syntax as save templates.</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="composite_default_label">
<property name="toolTip">
<string>Similar to save templates. For example, {title} {isbn}</string>
</property>
<property name="text">
<string>Default: (nothing)</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<widget class="QLabel" name="composite_label">
<property name="text">
<string>&amp;Template</string>
</property>
<property name="buddy">
<cstring>composite_box</cstring>
</property>
</widget>
</item>
<item row="10" column="0" colspan="3">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="3" column="0">
<item row="11" column="0">
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@ -184,6 +234,7 @@
<tabstop>column_heading_box</tabstop>
<tabstop>column_type_box</tabstop>
<tabstop>date_format_box</tabstop>
<tabstop>composite_box</tabstop>
<tabstop>button_box</tabstop>
</tabstops>
<resources/>

View File

@ -18,7 +18,7 @@ from calibre.utils.date import parse_date
class CustomColumns(object):
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
'int', 'float', 'bool', 'series'])
'int', 'float', 'bool', 'series', 'composite'])
def custom_table_names(self, num):
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
@ -540,7 +540,7 @@ class CustomColumns(object):
if datatype not in self.CUSTOM_DATA_TYPES:
raise ValueError('%r is not a supported data type'%datatype)
normalized = datatype not in ('datetime', 'comments', 'int', 'bool',
'float')
'float', 'composite')
is_multiple = is_multiple and datatype in ('text',)
num = self.conn.execute(
('INSERT INTO '
@ -551,7 +551,7 @@ class CustomColumns(object):
if datatype in ('rating', 'int'):
dt = 'INT'
elif datatype in ('text', 'comments', 'series'):
elif datatype in ('text', 'comments', 'series', 'composite'):
dt = 'TEXT'
elif datatype in ('float',):
dt = 'REAL'

View File

@ -538,6 +538,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.pubdate = self.pubdate(idx, index_is_id=index_is_id)
mi.uuid = self.uuid(idx, index_is_id=index_is_id)
mi.title_sort = self.title_sort(idx, index_is_id=index_is_id)
mi.formats = self.formats(idx, index_is_id=index_is_id).split(',')
tags = self.tags(idx, index_is_id=index_is_id)
if tags:
mi.tags = [i.strip() for i in tags.split(',')]

View File

@ -68,7 +68,7 @@ class FieldMetadata(dict):
'''
VALID_DATA_TYPES = frozenset([None, 'rating', 'text', 'comments', 'datetime',
'int', 'float', 'bool', 'series'])
'int', 'float', 'bool', 'series', 'composite'])
# Builtin metadata {{{

View File

@ -228,29 +228,19 @@ class MobileServer(object):
for key in CKEYS:
def concat(name, val):
return '%s:#:%s'%(name, unicode(val))
val = record[CFM[key]['rec_index']]
if val:
datatype = CFM[key]['datatype']
if datatype in ['comments']:
continue
name = CFM[key]['name']
if datatype == 'text' and CFM[key]['is_multiple']:
book[key] = concat(name,
format_tag_string(val, '|',
no_tag_count=True))
elif datatype == 'series':
book[key] = concat(name, '%s [%s]'%(val,
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
elif datatype == 'datetime':
book[key] = concat(name,
format_date(val, CFM[key]['display'].get('date_format','dd MMM yyyy')))
elif datatype == 'bool':
if val:
book[key] = concat(name, __builtin__._('Yes'))
else:
book[key] = concat(name, __builtin__._('No'))
else:
book[key] = concat(name, val)
mi = self.db.get_metadata(record[CFM['id']['rec_index']], index_is_id=True)
name, val = mi.format_field(key)
if val is None:
continue
datatype = CFM[key]['datatype']
if datatype in ['comments']:
continue
if datatype == 'text' and CFM[key]['is_multiple']:
book[key] = concat(name,
format_tag_string(val, ',',
no_tag_count=True))
else:
book[key] = concat(name, val)
updated = self.db.last_modified()

View File

@ -132,7 +132,8 @@ def CATALOG_GROUP_ENTRY(item, category, base_href, version, updated):
link
)
def ACQUISITION_ENTRY(item, version, FM, updated, CFM, CKEYS):
def ACQUISITION_ENTRY(item, version, db, updated, CFM, CKEYS):
FM = db.FIELD_MAP
title = item[FM['title']]
if not title:
title = _('Unknown')
@ -157,22 +158,16 @@ def ACQUISITION_ENTRY(item, version, FM, updated, CFM, CKEYS):
(series,
fmt_sidx(float(item[FM['series_index']]))))
for key in CKEYS:
val = item[CFM[key]['rec_index']]
mi = db.get_metadata(item[CFM['id']['rec_index']], index_is_id=True)
name, val = mi.format_field(key)
if val is not None:
name = CFM[key]['name']
datatype = CFM[key]['datatype']
if datatype == 'text' and CFM[key]['is_multiple']:
extra.append('%s: %s<br />'%(name, format_tag_string(val, '|',
extra.append('%s: %s<br />'%(name, format_tag_string(val, ',',
ignore_max=True,
no_tag_count=True)))
elif datatype == 'series':
extra.append('%s: %s [%s]<br />'%(name, val,
fmt_sidx(item[CFM.cc_series_index_column_for(key)])))
elif datatype == 'datetime':
extra.append('%s: %s<br />'%(name,
format_date(val, CFM[key]['display'].get('date_format','dd MMM yyyy'))))
else:
extra.append('%s: %s <br />' % (CFM[key]['name'], val))
extra.append('%s: %s<br />'%(name, val))
comments = item[FM['comments']]
if comments:
comments = comments_to_html(comments)
@ -280,13 +275,14 @@ class NavFeed(Feed):
class AcquisitionFeed(NavFeed):
def __init__(self, updated, id_, items, offsets, page_url, up_url, version,
FM, CFM):
db):
NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
CFM = db.field_metadata
CKEYS = [key for key in sorted(CFM.get_custom_fields(),
cmp=lambda x,y: cmp(CFM[x]['name'].lower(),
CFM[y]['name'].lower()))]
for item in items:
self.root.append(ACQUISITION_ENTRY(item, version, FM, updated,
self.root.append(ACQUISITION_ENTRY(item, version, db, updated,
CFM, CKEYS))
class CategoryFeed(NavFeed):
@ -384,7 +380,7 @@ class OPDSServer(object):
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
cherrypy.response.headers['Content-Type'] = 'application/atom+xml;profile=opds-catalog'
return str(AcquisitionFeed(updated, id_, items, offsets,
page_url, up_url, version, self.db.FIELD_MAP, self.db.field_metadata))
page_url, up_url, version, self.db))
def opds_search(self, query=None, version=0, offset=0):
try:

View File

@ -102,31 +102,21 @@ class XMLServer(object):
for key in CKEYS:
def concat(name, val):
return '%s:#:%s'%(name, unicode(val))
val = record[CFM[key]['rec_index']]
if val:
datatype = CFM[key]['datatype']
if datatype in ['comments']:
continue
k = str('CF_'+key[1:])
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))
elif datatype == 'series':
kwargs[k] = concat(name, '%s [%s]'%(val,
fmt_sidx(record[CFM.cc_series_index_column_for(key)])))
elif datatype == 'datetime':
kwargs[k] = concat(name,
format_date(val, CFM[key]['display'].get('date_format','dd MMM yyyy')))
elif datatype == 'bool':
if val:
kwargs[k] = concat(name, __builtin__._('Yes'))
else:
kwargs[k] = concat(name, __builtin__._('No'))
else:
kwargs[k] = concat(name, val)
mi = self.db.get_metadata(record[CFM['id']['rec_index']], index_is_id=True)
name, val = mi.format_field(key)
if not val:
continue
datatype = CFM[key]['datatype']
if datatype in ['comments']:
continue
k = str('CF_'+key[1:])
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))
else:
kwargs[k] = concat(name, val)
kwargs['custcols'] = ','.join(custcols)
books.append(E.book(c, **kwargs))