From 6b80d5d0311ffb2f5d20f27aa21b2351efdeae6a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 20 Jun 2010 11:25:24 +0100 Subject: [PATCH] Series type: attempt using 1 column with a virtual second column --- .../dialogs/config/create_custom_column.py | 13 ++-- src/calibre/gui2/library/models.py | 26 ++++++-- src/calibre/library/caches.py | 42 +++++++----- src/calibre/library/custom_columns.py | 66 +++++++++++++++---- src/calibre/library/database2.py | 5 ++ src/calibre/library/field_metadata.py | 4 +- 6 files changed, 117 insertions(+), 39 deletions(-) diff --git a/src/calibre/gui2/dialogs/config/create_custom_column.py b/src/calibre/gui2/dialogs/config/create_custom_column.py index a66b7b6642..2aae567b1c 100644 --- a/src/calibre/gui2/dialogs/config/create_custom_column.py +++ b/src/calibre/gui2/dialogs/config/create_custom_column.py @@ -24,16 +24,19 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): 2:{'datatype':'comments', 'text':_('Long text, like comments, not shown in the tag browser'), '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}, - 4:{'datatype':'float', + 5:{'datatype':'float', 'text':_('Floating point numbers'), 'is_multiple':False}, - 5:{'datatype':'int', + 6:{'datatype':'int', 'text':_('Integers'), 'is_multiple':False}, - 6:{'datatype':'rating', + 7:{'datatype':'rating', 'text':_('Ratings, shown with stars'), 'is_multiple':False}, - 7:{'datatype':'bool', + 8:{'datatype':'bool', 'text':_('Yes/No'), 'is_multiple':False}, } diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index dcc338dbdc..435b5c4c07 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -520,7 +520,7 @@ class BooksModel(QAbstractTableModel): # {{{ return QVariant(', '.join(sorted(tags.split(',')))) return None - def series(r, idx=-1, siix=-1): + def series_type(r, idx=-1, siix=-1): series = self.db.data[r][idx] if series: 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), 'tags' : functools.partial(tags, 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'], siix=self.db.field_metadata['series_index']['rec_index']), 'ondevice' : functools.partial(text_type, @@ -620,6 +620,9 @@ class BooksModel(QAbstractTableModel): # {{{ 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)) 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 @@ -681,6 +684,8 @@ class BooksModel(QAbstractTableModel): # {{{ def set_custom_column_data(self, row, colhead, value): typ = self.custom_columns[colhead]['datatype'] + label=self.db.field_metadata.key_to_label(colhead) + s_index = None if typ in ('text', 'comments'): val = unicode(value.toString()).strip() val = val if val else None @@ -702,9 +707,20 @@ class BooksModel(QAbstractTableModel): # {{{ if not val.isValid(): return False val = qt_to_dt(val, as_utc=False) - self.db.set_custom(self.db.id(row), val, - label=self.db.field_metadata.key_to_label(colhead), - num=None, append=False, notify=True) + elif typ == 'series': + val = unicode(value.toString()).strip() + 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 def setData(self, index, value, role): diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 57f9d0baaf..d4afaabcdc 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -401,7 +401,8 @@ class ResultCache(SearchQueryParser): for x in self.field_metadata: if len(self.field_metadata[x]['search_terms']): 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]) col_datatype[db_col[x]] = self.field_metadata[x]['datatype'] is_multiple_cols[db_col[x]] = self.field_metadata[x]['is_multiple'] @@ -580,16 +581,18 @@ class ResultCache(SearchQueryParser): self.sort(field, ascending) self._map_filtered = list(self._map) - def seriescmp(self, x, y): - sidx = self.FIELD_MAP['series'] + def seriescmp(self, sidx, siidx, x, y, library_order=None): try: - ans = cmp(title_sort(self._data[x][sidx].lower()), - title_sort(self._data[y][sidx].lower())) + if library_order: + 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 ans = cmp(self._data[x][sidx], self._data[y][sidx]) if ans != 0: return ans - sidx = self.FIELD_MAP['series_index'] - return cmp(self._data[x][sidx], self._data[y][sidx]) + return cmp(self._data[x][siidx], self._data[y][siidx]) def cmp(self, loc, x, y, asstr=True, subsort=False): try: @@ -618,18 +621,27 @@ class ResultCache(SearchQueryParser): elif field == 'authors': field = 'author_sort' as_string = field not in ('size', 'rating', 'timestamp') if self.field_metadata[field]['is_custom']: - as_string = self.field_metadata[field]['datatype'] in ('comments', 'text') - field = self.field_metadata[field]['colnum'] + if self.field_metadata[field]['datatype'] == 'series': + 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: subsort = True 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_filtered = [id for id in self._map if id in self._map_filtered] diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index c0ba91e252..a5f9ddb4f0 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en' import json from functools import partial +from math import floor from calibre import prints from calibre.constants import preferred_encoding @@ -16,7 +17,7 @@ from calibre.utils.date import parse_date class CustomColumns(object): CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime', - 'int', 'float', 'bool']) + 'int', 'float', 'bool', 'series']) def custom_table_names(self, num): return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num @@ -137,7 +138,8 @@ class CustomColumns(object): 'bool': adapt_bool, 'comments': lambda x,d: adapt_text(x, {'is_multiple':False}), 'datetime' : adapt_datetime, - 'text':adapt_text + 'text':adapt_text, + 'series':adapt_text } # Create Tag Browser categories for custom columns @@ -220,6 +222,28 @@ class CustomColumns(object): self.conn.commit() # 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): if label is not None: data = self.custom_column_label_map[label] @@ -271,9 +295,8 @@ class CustomColumns(object): self.conn.commit() return changed - - - def set_custom(self, id_, val, label=None, num=None, append=False, notify=True): + def set_custom(self, id_, val, extra=None, label=None, num=None, + append=False, notify=True): if label is not None: data = self.custom_column_label_map[label] if num is not None: @@ -317,10 +340,17 @@ class CustomColumns(object): 'INSERT INTO %s(value) VALUES(?)'%table, (x,)).lastrowid if not self.conn.get( 'SELECT book FROM %s WHERE book=? AND value=?'%lt, - (id_, xid), all=False): - self.conn.execute( - 'INSERT INTO %s(book, value) VALUES (?,?)'%lt, - (id_, xid)) + (id_, xid), all=False): + if data['datatype'] == 'series': + self.conn.execute( + '''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() nval = self.conn.get( '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) custom_{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: line = ''' (SELECT value FROM {table} WHERE book=books.id) custom_{num} @@ -393,7 +426,7 @@ class CustomColumns(object): if datatype in ('rating', 'int'): dt = 'INT' - elif datatype in ('text', 'comments'): + elif datatype in ('text', 'comments', 'series'): dt = 'TEXT' elif datatype in ('float',): dt = 'REAL' @@ -404,6 +437,10 @@ class CustomColumns(object): collate = 'COLLATE NOCASE' if dt == 'TEXT' else '' table, lt = self.custom_table_names(num) if normalized: + if datatype == 'series': + s_index = 's_index REAL,' + else: + s_index = '' lines = [ '''\ CREATE TABLE %s( @@ -419,8 +456,9 @@ class CustomColumns(object): id INTEGER PRIMARY KEY AUTOINCREMENT, book INTEGER NOT NULL, value INTEGER NOT NULL, + %s UNIQUE(book, value) - );'''%lt, + );'''%(lt, s_index), 'CREATE INDEX %s_aidx ON %s (value);'%(lt,lt), 'CREATE INDEX %s_bidx ON %s (book);'%(lt,lt), @@ -467,7 +505,8 @@ class CustomColumns(object): books_ratings_link as bl, ratings as r 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}; CREATE VIEW tag_browser_filtered_{table} AS SELECT @@ -481,7 +520,8 @@ class CustomColumns(object): ratings as r WHERE {lt}.value={table}.id AND bl.book={lt}.book 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}; '''.format(lt=lt, table=table), diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index fe4aac12b5..7e4a879654 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -237,6 +237,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.custom_column_num_map[col]['label'], base, 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_metadata.set_field_record_index('cover', base+1, prefer_custom=False) diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 8cb5c9bdad..5ccc17d1eb 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -81,7 +81,7 @@ class FieldMetadata(dict): 'column':'name', 'link_column':'series', 'category_sort':'(title_sort(name))', - 'datatype':'text', + 'datatype':'series', 'is_multiple':None, 'kind':'field', 'name':_('Series'), @@ -398,6 +398,8 @@ class FieldMetadata(dict): if val['is_category'] and val['kind'] in ('user', 'search'): 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): if label in self._tb_cats: