Series type: attempt using 1 column with a virtual second column

This commit is contained in:
Charles Haley 2010-06-20 11:25:24 +01:00
parent eaf68c0786
commit 6b80d5d031
6 changed files with 117 additions and 39 deletions

View File

@ -24,16 +24,19 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
2:{'datatype':'comments', 2:{'datatype':'comments',
'text':_('Long text, like comments, not shown in the tag browser'), 'text':_('Long text, like comments, not shown in the tag browser'),
'is_multiple':False}, 'is_multiple':False},
3:{'datatype':'datetime', 3:{'datatype':'series',
'text':_('Text column for keeping series-like information'),
'is_multiple':False},
4:{'datatype':'datetime',
'text':_('Date'), 'is_multiple':False}, 'text':_('Date'), 'is_multiple':False},
4:{'datatype':'float', 5:{'datatype':'float',
'text':_('Floating point numbers'), 'is_multiple':False}, 'text':_('Floating point numbers'), 'is_multiple':False},
5:{'datatype':'int', 6:{'datatype':'int',
'text':_('Integers'), 'is_multiple':False}, 'text':_('Integers'), 'is_multiple':False},
6:{'datatype':'rating', 7:{'datatype':'rating',
'text':_('Ratings, shown with stars'), 'text':_('Ratings, shown with stars'),
'is_multiple':False}, 'is_multiple':False},
7:{'datatype':'bool', 8:{'datatype':'bool',
'text':_('Yes/No'), 'is_multiple':False}, 'text':_('Yes/No'), 'is_multiple':False},
} }

View File

@ -520,7 +520,7 @@ class BooksModel(QAbstractTableModel): # {{{
return QVariant(', '.join(sorted(tags.split(',')))) return QVariant(', '.join(sorted(tags.split(','))))
return None return None
def series(r, idx=-1, siix=-1): def series_type(r, idx=-1, siix=-1):
series = self.db.data[r][idx] series = self.db.data[r][idx]
if series: if series:
idx = fmt_sidx(self.db.data[r][siix]) idx = fmt_sidx(self.db.data[r][siix])
@ -591,7 +591,7 @@ class BooksModel(QAbstractTableModel): # {{{
idx=self.db.field_metadata['publisher']['rec_index'], mult=False), idx=self.db.field_metadata['publisher']['rec_index'], mult=False),
'tags' : functools.partial(tags, 'tags' : functools.partial(tags,
idx=self.db.field_metadata['tags']['rec_index']), idx=self.db.field_metadata['tags']['rec_index']),
'series' : functools.partial(series, 'series' : functools.partial(series_type,
idx=self.db.field_metadata['series']['rec_index'], idx=self.db.field_metadata['series']['rec_index'],
siix=self.db.field_metadata['series_index']['rec_index']), siix=self.db.field_metadata['series_index']['rec_index']),
'ondevice' : functools.partial(text_type, 'ondevice' : functools.partial(text_type,
@ -620,6 +620,9 @@ class BooksModel(QAbstractTableModel): # {{{
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes') bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes')
elif datatype == 'rating': elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx) 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))
else: else:
print 'What type is this?', col, datatype print 'What type is this?', col, datatype
# build a index column to data converter map, to remove the string lookup in the data loop # build a index column to data converter map, to remove the string lookup in the data loop
@ -681,6 +684,8 @@ class BooksModel(QAbstractTableModel): # {{{
def set_custom_column_data(self, row, colhead, value): def set_custom_column_data(self, row, colhead, value):
typ = self.custom_columns[colhead]['datatype'] typ = self.custom_columns[colhead]['datatype']
label=self.db.field_metadata.key_to_label(colhead)
s_index = None
if typ in ('text', 'comments'): if typ in ('text', 'comments'):
val = unicode(value.toString()).strip() val = unicode(value.toString()).strip()
val = val if val else None val = val if val else None
@ -702,9 +707,20 @@ class BooksModel(QAbstractTableModel): # {{{
if not val.isValid(): if not val.isValid():
return False return False
val = qt_to_dt(val, as_utc=False) val = qt_to_dt(val, as_utc=False)
self.db.set_custom(self.db.id(row), val, elif typ == 'series':
label=self.db.field_metadata.key_to_label(colhead), val = unicode(value.toString()).strip()
num=None, append=False, notify=True) pat = re.compile(r'\[([.0-9]+)\]')
match = pat.search(val)
if match is not None:
val = pat.sub('', val).strip()
s_index = float(match.group(1))
elif val:
if tweaks['series_index_auto_increment'] == 'next':
s_index = self.db.get_next_cc_series_num_for(val, label=label)
else:
s_index = 1.0
self.db.set_custom(self.db.id(row), val, extra=s_index,
label=label, num=None, append=False, notify=True)
return True return True
def setData(self, index, value, role): def setData(self, index, value, role):

View File

@ -401,7 +401,8 @@ class ResultCache(SearchQueryParser):
for x in self.field_metadata: for x in self.field_metadata:
if len(self.field_metadata[x]['search_terms']): if len(self.field_metadata[x]['search_terms']):
db_col[x] = self.field_metadata[x]['rec_index'] db_col[x] = self.field_metadata[x]['rec_index']
if self.field_metadata[x]['datatype'] not in ['text', 'comments']: if self.field_metadata[x]['datatype'] not in \
['text', 'comments', 'series']:
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']
@ -580,16 +581,18 @@ class ResultCache(SearchQueryParser):
self.sort(field, ascending) self.sort(field, ascending)
self._map_filtered = list(self._map) self._map_filtered = list(self._map)
def seriescmp(self, x, y): def seriescmp(self, sidx, siidx, x, y, library_order=None):
sidx = self.FIELD_MAP['series']
try: try:
ans = cmp(title_sort(self._data[x][sidx].lower()), if library_order:
title_sort(self._data[y][sidx].lower())) ans = cmp(title_sort(self._data[x][sidx].lower()),
title_sort(self._data[y][sidx].lower()))
else:
ans = cmp(self._data[x][sidx].lower(),
self._data[y][sidx].lower())
except AttributeError: # Some entries may be None except AttributeError: # Some entries may be None
ans = cmp(self._data[x][sidx], self._data[y][sidx]) ans = cmp(self._data[x][sidx], self._data[y][sidx])
if ans != 0: return ans if ans != 0: return ans
sidx = self.FIELD_MAP['series_index'] return cmp(self._data[x][siidx], self._data[y][siidx])
return cmp(self._data[x][sidx], self._data[y][sidx])
def cmp(self, loc, x, y, asstr=True, subsort=False): def cmp(self, loc, x, y, asstr=True, subsort=False):
try: try:
@ -618,18 +621,27 @@ class ResultCache(SearchQueryParser):
elif field == 'authors': field = 'author_sort' elif field == 'authors': field = 'author_sort'
as_string = field not in ('size', 'rating', 'timestamp') as_string = field not in ('size', 'rating', 'timestamp')
if self.field_metadata[field]['is_custom']: if self.field_metadata[field]['is_custom']:
as_string = self.field_metadata[field]['datatype'] in ('comments', 'text') if self.field_metadata[field]['datatype'] == 'series':
field = self.field_metadata[field]['colnum'] fcmp = functools.partial(self.seriescmp,
self.field_metadata[field]['rec_index'],
self.field_metadata.cc_series_index_column_for(field),
library_order=tweaks['title_series_sorting'] == 'library_order')
else:
as_string = self.field_metadata[field]['datatype'] in ('comments', 'text')
field = self.field_metadata[field]['colnum']
fcmp = functools.partial(self.cmp, self.FIELD_MAP[field],
subsort=subsort, asstr=as_string)
elif field == 'series':
fcmp = functools.partial(self.seriescmp, self.FIELD_MAP['series'],
self.FIELD_MAP['series_index'],
library_order=tweaks['title_series_sorting'] == 'library_order')
else:
fcmp = functools.partial(self.cmp, self.FIELD_MAP[field],
subsort=subsort, asstr=as_string)
if self.first_sort: if self.first_sort:
subsort = True subsort = True
self.first_sort = False self.first_sort = False
fcmp = self.seriescmp \
if field == 'series' and \
tweaks['title_series_sorting'] == 'library_order' \
else \
functools.partial(self.cmp, self.FIELD_MAP[field],
subsort=subsort, asstr=as_string)
self._map.sort(cmp=fcmp, reverse=not ascending) self._map.sort(cmp=fcmp, reverse=not ascending)
self._map_filtered = [id for id in self._map if id in self._map_filtered] self._map_filtered = [id for id in self._map if id in self._map_filtered]

View File

@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
import json import json
from functools import partial from functools import partial
from math import floor
from calibre import prints from calibre import prints
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
@ -16,7 +17,7 @@ from calibre.utils.date import parse_date
class CustomColumns(object): class CustomColumns(object):
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime', CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
'int', 'float', 'bool']) 'int', 'float', 'bool', 'series'])
def custom_table_names(self, num): def custom_table_names(self, num):
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
@ -137,7 +138,8 @@ class CustomColumns(object):
'bool': adapt_bool, 'bool': adapt_bool,
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}), 'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
'datetime' : adapt_datetime, 'datetime' : adapt_datetime,
'text':adapt_text 'text':adapt_text,
'series':adapt_text
} }
# Create Tag Browser categories for custom columns # Create Tag Browser categories for custom columns
@ -220,6 +222,28 @@ class CustomColumns(object):
self.conn.commit() self.conn.commit()
# end convenience methods # end convenience methods
def get_next_cc_series_num_for(self, series, label=None, num=None):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
if data['datatype'] != 'series':
return None
table, lt = self.custom_table_names(data['num'])
# get the id of the row containing the series string
series_id = self.conn.get('SELECT id from %s WHERE value=?'%table,
(series,), all=False)
if series_id is None:
return 1.0
# get the label of the associated series number table
series_num = self.conn.get('''
SELECT MAX({lt}.s_index) FROM {lt}
WHERE {lt}.book IN (SELECT book FROM {lt} where value=?)
'''.format(lt=lt), (series_id,), all=False)
if series_num is None:
return 1.0
return floor(series_num+1)
def all_custom(self, label=None, num=None): def all_custom(self, label=None, num=None):
if label is not None: if label is not None:
data = self.custom_column_label_map[label] data = self.custom_column_label_map[label]
@ -271,9 +295,8 @@ class CustomColumns(object):
self.conn.commit() self.conn.commit()
return changed return changed
def set_custom(self, id_, val, extra=None, label=None, num=None,
append=False, notify=True):
def set_custom(self, id_, val, label=None, num=None, append=False, notify=True):
if label is not None: if label is not None:
data = self.custom_column_label_map[label] data = self.custom_column_label_map[label]
if num is not None: if num is not None:
@ -317,10 +340,17 @@ class CustomColumns(object):
'INSERT INTO %s(value) VALUES(?)'%table, (x,)).lastrowid 'INSERT INTO %s(value) VALUES(?)'%table, (x,)).lastrowid
if not self.conn.get( if not self.conn.get(
'SELECT book FROM %s WHERE book=? AND value=?'%lt, 'SELECT book FROM %s WHERE book=? AND value=?'%lt,
(id_, xid), all=False): (id_, xid), all=False):
self.conn.execute( if data['datatype'] == 'series':
'INSERT INTO %s(book, value) VALUES (?,?)'%lt, self.conn.execute(
(id_, xid)) '''INSERT INTO %s(book, value, s_index)
VALUES (?,?,?)'''%lt, (id_, xid, extra))
self.data.set(id_, self.FIELD_MAP[data['num']]+1,
extra, row_is_id=True)
else:
self.conn.execute(
'''INSERT INTO %s(book, value)
VALUES (?,?)'''%lt, (id_, xid))
self.conn.commit() self.conn.commit()
nval = self.conn.get( nval = self.conn.get(
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'], 'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
@ -370,6 +400,9 @@ class CustomColumns(object):
{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}
'''.format(query=query%table, lt=lt, table=table, num=data['num']) '''.format(query=query%table, lt=lt, table=table, num=data['num'])
if data['datatype'] == 'series':
line += ''',(SELECT s_index FROM {lt} WHERE {lt}.book=books.id)
custom_index_{num}'''.format(lt=lt, num=data['num'])
else: else:
line = ''' line = '''
(SELECT value FROM {table} WHERE book=books.id) custom_{num} (SELECT value FROM {table} WHERE book=books.id) custom_{num}
@ -393,7 +426,7 @@ class CustomColumns(object):
if datatype in ('rating', 'int'): if datatype in ('rating', 'int'):
dt = 'INT' dt = 'INT'
elif datatype in ('text', 'comments'): elif datatype in ('text', 'comments', 'series'):
dt = 'TEXT' dt = 'TEXT'
elif datatype in ('float',): elif datatype in ('float',):
dt = 'REAL' dt = 'REAL'
@ -404,6 +437,10 @@ class CustomColumns(object):
collate = 'COLLATE NOCASE' if dt == 'TEXT' else '' collate = 'COLLATE NOCASE' if dt == 'TEXT' else ''
table, lt = self.custom_table_names(num) table, lt = self.custom_table_names(num)
if normalized: if normalized:
if datatype == 'series':
s_index = 's_index REAL,'
else:
s_index = ''
lines = [ lines = [
'''\ '''\
CREATE TABLE %s( CREATE TABLE %s(
@ -419,8 +456,9 @@ class CustomColumns(object):
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
book INTEGER NOT NULL, book INTEGER NOT NULL,
value INTEGER NOT NULL, value INTEGER NOT NULL,
%s
UNIQUE(book, value) UNIQUE(book, value)
);'''%lt, );'''%(lt, s_index),
'CREATE INDEX %s_aidx ON %s (value);'%(lt,lt), 'CREATE INDEX %s_aidx ON %s (value);'%(lt,lt),
'CREATE INDEX %s_bidx ON %s (book);'%(lt,lt), 'CREATE INDEX %s_bidx ON %s (book);'%(lt,lt),
@ -467,7 +505,8 @@ class CustomColumns(object):
books_ratings_link as bl, books_ratings_link as bl,
ratings as r ratings as r
WHERE {lt}.value={table}.id and bl.book={lt}.book and WHERE {lt}.value={table}.id and bl.book={lt}.book and
r.id = bl.rating and r.rating <> 0) avg_rating r.id = bl.rating and r.rating <> 0) avg_rating,
value as sort
FROM {table}; FROM {table};
CREATE VIEW tag_browser_filtered_{table} AS SELECT CREATE VIEW tag_browser_filtered_{table} AS SELECT
@ -481,7 +520,8 @@ class CustomColumns(object):
ratings as r ratings as r
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
r.id = bl.rating AND r.rating <> 0 AND r.id = bl.rating AND r.rating <> 0 AND
books_list_filter(bl.book)) avg_rating books_list_filter(bl.book)) avg_rating,
value as sort
FROM {table}; FROM {table};
'''.format(lt=lt, table=table), '''.format(lt=lt, table=table),

View File

@ -237,6 +237,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.custom_column_num_map[col]['label'], self.custom_column_num_map[col]['label'],
base, base,
prefer_custom=True) prefer_custom=True)
if self.custom_column_num_map[col]['datatype'] == 'series':
# account for the series index column. Field_metadata knows that
# the series index is one larger than the series. If you change
# it here, be sure to change it there as well.
self.FIELD_MAP[str(col)+'_s_index'] = base = base+1
self.FIELD_MAP['cover'] = base+1 self.FIELD_MAP['cover'] = base+1
self.field_metadata.set_field_record_index('cover', base+1, prefer_custom=False) self.field_metadata.set_field_record_index('cover', base+1, prefer_custom=False)

View File

@ -81,7 +81,7 @@ class FieldMetadata(dict):
'column':'name', 'column':'name',
'link_column':'series', 'link_column':'series',
'category_sort':'(title_sort(name))', 'category_sort':'(title_sort(name))',
'datatype':'text', 'datatype':'series',
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
'name':_('Series'), 'name':_('Series'),
@ -398,6 +398,8 @@ class FieldMetadata(dict):
if val['is_category'] and val['kind'] in ('user', 'search'): if val['is_category'] and val['kind'] in ('user', 'search'):
del self._tb_cats[key] del self._tb_cats[key]
def cc_series_index_column_for(self, key):
return self._tb_cats[key]['rec_index'] + 1
def add_user_category(self, label, name): def add_user_category(self, label, name):
if label in self._tb_cats: if label in self._tb_cats: