diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index 9d108d3807..4d0dd07746 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -371,7 +371,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): hidden_cols = state['hidden_columns'] positions = state['column_positions'] colmap.sort(cmp=lambda x,y: cmp(positions[x], positions[y])) - self.custcols = copy.deepcopy(self.db.custom_column_label_map) + self.custcols = copy.deepcopy(self.db.field_metadata.get_custom_field_metadata()) for col in colmap: item = QListWidgetItem(self.model.headers[col], self.columns) item.setData(Qt.UserRole, QVariant(col)) @@ -713,20 +713,20 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): must_restart = False for c in self.custcols: - if self.custcols[c]['num'] is None: + if self.custcols[c]['colnum'] is None: self.db.create_custom_column( - label=c, + label=self.custcols[c]['label'], name=self.custcols[c]['name'], datatype=self.custcols[c]['datatype'], is_multiple=self.custcols[c]['is_multiple'], display = self.custcols[c]['display']) must_restart = True elif '*deleteme' in self.custcols[c]: - self.db.delete_custom_column(label=c) + self.db.delete_custom_column(label=self.custcols[c]['label']) must_restart = True elif '*edited' in self.custcols[c]: cc = self.custcols[c] - self.db.set_custom_column_metadata(cc['num'], name=cc['name'], + self.db.set_custom_column_metadata(cc['colnum'], name=cc['name'], label=cc['label'], display = self.custcols[c]['display']) if '*must_restart' in self.custcols[c]: diff --git a/src/calibre/gui2/dialogs/config/create_custom_column.py b/src/calibre/gui2/dialogs/config/create_custom_column.py index 9e040315c9..693f079d12 100644 --- a/src/calibre/gui2/dialogs/config/create_custom_column.py +++ b/src/calibre/gui2/dialogs/config/create_custom_column.py @@ -69,13 +69,14 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): self.column_name_box.setText(c['label']) self.column_heading_box.setText(c['name']) ct = c['datatype'] if not c['is_multiple'] else '*text' - self.orig_column_number = c['num'] + self.orig_column_number = c['colnum'] self.orig_column_name = col column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x), self.column_types)) self.column_type_box.setCurrentIndex(column_numbers[ct]) self.column_type_box.setEnabled(False) if ct == 'datetime': - self.date_format_box.setText(c['display'].get('date_format', '')) + if c['display'].get('date_format', None): + self.date_format_box.setText(c['display'].get('date_format', '')) self.datatype_changed() self.exec_() @@ -90,7 +91,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): def accept(self): - col = unicode(self.column_name_box.text()) + col = unicode(self.column_name_box.text()).lower() + if not col.isalnum(): + return self.simple_error('', _('The label must contain only letters and digits')) col_heading = unicode(self.column_heading_box.text()) col_type = self.column_types[self.column_type_box.currentIndex()]['datatype'] if col_type == '*text': @@ -104,14 +107,14 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): return self.simple_error('', _('No column heading was provided')) bad_col = False if col in self.parent.custcols: - if not self.editing_col or self.parent.custcols[col]['num'] != self.orig_column_number: + if not self.editing_col or self.parent.custcols[col]['colnum'] != self.orig_column_number: 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: - if not self.editing_col or self.parent.custcols[t]['num'] != self.orig_column_number: + if not self.editing_col or self.parent.custcols[t]['colnum'] != self.orig_column_number: bad_head = True for t in self.standard_colheads: if self.standard_colheads[t] == col_heading: @@ -129,14 +132,15 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): date_format = {'date_format': None} if not self.editing_col: - self.parent.custcols[col] = { + self.parent.db.field_metadata + self.parent.custcols[self.parent.db.field_metadata.custom_field_prefix+col] = { 'label':col, 'name':col_heading, 'datatype':col_type, 'editable':True, 'display':date_format, 'normalized':None, - 'num':None, + 'colnum':None, 'is_multiple':is_multiple, } item = QListWidgetItem(col_heading, self.parent.columns) diff --git a/src/calibre/gui2/dialogs/config/create_custom_column.ui b/src/calibre/gui2/dialogs/config/create_custom_column.ui index 279349f28e..5cb9494845 100644 --- a/src/calibre/gui2/dialogs/config/create_custom_column.ui +++ b/src/calibre/gui2/dialogs/config/create_custom_column.ui @@ -65,7 +65,7 @@ - Used for searching the column. Must be lower case and not contain spaces or colons. + Used for searching the column. Must contain only digits and lower case letters. diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index d908ed01b4..529055ecd2 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -171,7 +171,8 @@ class TagsDelegate(QStyledItemDelegate): # {{{ if not index.model().is_custom_column(col): editor = TagsLineEdit(parent, self.db.all_tags()) else: - editor = TagsLineEdit(parent, sorted(list(self.db.all_custom(label=col)))) + editor = TagsLineEdit(parent, + sorted(list(self.db.all_custom(label=self.db.field_metadata.key_to_label(col))))) return editor else: editor = EnLineEdit(parent) @@ -209,7 +210,7 @@ class CcDateDelegate(QStyledItemDelegate): # {{{ m = index.model() # db col is not named for the field, but for the table number. To get it, # gui column -> column label -> table number -> db column - val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[m.column_map[index.column()]]['num']]] + val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']] if val is None: val = now() editor.setDate(val) @@ -243,7 +244,7 @@ class CcTextDelegate(QStyledItemDelegate): # {{{ editor.setDecimals(2) else: editor = EnLineEdit(parent) - complete_items = sorted(list(m.db.all_custom(label=col))) + complete_items = sorted(list(m.db.all_custom(label=m.db.field_metadata.key_to_label(col)))) completer = QCompleter(complete_items, self) completer.setCaseSensitivity(Qt.CaseInsensitive) completer.setCompletionMode(QCompleter.PopupCompletion) @@ -260,9 +261,7 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{ def createEditor(self, parent, option, index): m = index.model() col = m.column_map[index.column()] - # db col is not named for the field, but for the table number. To get it, - # gui column -> column label -> table number -> db column - text = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[col]['num']]] + text = m.db.data[index.row()][m.custom_columns[col]['rec_index']] editor = CommentsDialog(parent, text) d = editor.exec_() if d: @@ -297,9 +296,7 @@ class CcBoolDelegate(QStyledItemDelegate): # {{{ def setEditorData(self, editor, index): m = index.model() - # db col is not named for the field, but for the table number. To get it, - # gui column -> column label -> table number -> db column - val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[m.column_map[index.column()]]['num']]] + val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']] if tweaks['bool_custom_columns_are_tristate'] == 'no': val = 1 if not val else 0 else: diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 072f81e2d1..5490e96169 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -111,15 +111,15 @@ class BooksModel(QAbstractTableModel): # {{{ def set_database(self, db): self.db = db - self.custom_columns = self.db.custom_column_label_map + self.custom_columns = self.db.field_metadata.get_custom_field_metadata() self.column_map = list(self.orig_headers.keys()) + \ list(self.custom_columns) def col_idx(name): if name == 'ondevice': return -1 - if name not in self.db.FIELD_MAP: + if name not in self.db.field_metadata: return 100000 - return self.db.FIELD_MAP[name] + return self.db.field_metadata[name]['rec_index'] self.column_map.sort(cmp=lambda x,y: cmp(col_idx(x), col_idx(y))) for col in self.column_map: @@ -232,11 +232,12 @@ class BooksModel(QAbstractTableModel): # {{{ return self.about_to_be_sorted.emit(self.db.id) ascending = order == Qt.AscendingOrder - self.db.sort(self.column_map[col], ascending) + label = self.column_map[col] + self.db.sort(label, ascending) if reset: self.clear_caches() self.reset() - self.sorted_on = (self.column_map[col], order) + self.sorted_on = (label, order) self.sort_history.insert(0, self.sorted_on) self.sorting_done.emit(self.db.index) @@ -551,36 +552,36 @@ class BooksModel(QAbstractTableModel): # {{{ self.dc = { 'title' : functools.partial(text_type, - idx=self.db.FIELD_MAP['title'], mult=False), + idx=self.db.field_metadata['title']['rec_index'], mult=False), 'authors' : functools.partial(authors, - idx=self.db.FIELD_MAP['authors']), + idx=self.db.field_metadata['authors']['rec_index']), 'size' : functools.partial(size, - idx=self.db.FIELD_MAP['size']), + idx=self.db.field_metadata['size']['rec_index']), 'timestamp': functools.partial(datetime_type, - idx=self.db.FIELD_MAP['timestamp']), + idx=self.db.field_metadata['timestamp']['rec_index']), 'pubdate' : functools.partial(datetime_type, - idx=self.db.FIELD_MAP['pubdate']), + idx=self.db.field_metadata['pubdate']['rec_index']), 'rating' : functools.partial(rating_type, - idx=self.db.FIELD_MAP['rating']), + idx=self.db.field_metadata['rating']['rec_index']), 'publisher': functools.partial(text_type, - idx=self.db.FIELD_MAP['publisher'], mult=False), + idx=self.db.field_metadata['publisher']['rec_index'], mult=False), 'tags' : functools.partial(tags, - idx=self.db.FIELD_MAP['tags']), + idx=self.db.field_metadata['tags']['rec_index']), 'series' : functools.partial(series, - idx=self.db.FIELD_MAP['series'], - siix=self.db.FIELD_MAP['series_index']), + idx=self.db.field_metadata['series']['rec_index'], + siix=self.db.field_metadata['series_index']['rec_index']), 'ondevice' : functools.partial(text_type, - idx=self.db.FIELD_MAP['ondevice'], mult=False), + idx=self.db.field_metadata['ondevice']['rec_index'], mult=False), } self.dc_decorator = { 'ondevice':functools.partial(ondevice_decorator, - idx=self.db.FIELD_MAP['ondevice']), + idx=self.db.field_metadata['ondevice']['rec_index']), } # Add the custom columns to the data converters for col in self.custom_columns: - idx = self.db.FIELD_MAP[self.custom_columns[col]['num']] + 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']) @@ -632,8 +633,6 @@ class BooksModel(QAbstractTableModel): # {{{ return None if role == Qt.ToolTipRole: ht = self.column_map[section] - if self.is_custom_column(self.column_map[section]): - ht = self.db.field_metadata.custom_field_prefix + ht if ht == 'timestamp': # change help text because users know this field as 'date' ht = 'date' return QVariant(_('The lookup/search name is "{0}"').format(ht)) @@ -652,7 +651,7 @@ class BooksModel(QAbstractTableModel): # {{{ if colhead in self.editable_cols: flags |= Qt.ItemIsEditable elif self.is_custom_column(colhead): - if self.custom_columns[colhead]['editable']: + if self.custom_columns[colhead]['is_editable']: flags |= Qt.ItemIsEditable return flags @@ -679,7 +678,9 @@ 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=colhead, num=None, append=False, notify=True) + self.db.set_custom(self.db.id(row), val, + label=self.db.field_metadata.key_to_label(colhead), + 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 47529f223f..93891ee92b 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -150,9 +150,8 @@ class ResultCache(SearchQueryParser): ''' Stores sorted and filtered metadata in memory. ''' - def __init__(self, FIELD_MAP, cc_label_map, field_metadata): + def __init__(self, FIELD_MAP, field_metadata): self.FIELD_MAP = FIELD_MAP - self.custom_column_label_map = cc_label_map self._map = self._map_filtered = self._data = [] self.first_sort = True self.search_restriction = '' @@ -598,9 +597,9 @@ class ResultCache(SearchQueryParser): elif field == 'title': field = 'sort' elif field == 'authors': field = 'author_sort' as_string = field not in ('size', 'rating', 'timestamp') - if field in self.custom_column_label_map: - as_string = self.custom_column_label_map[field]['datatype'] in ('comments', 'text') - field = self.custom_column_label_map[field]['num'] + 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.first_sort: subsort = True diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 7a63f37588..86b7d715ea 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -154,8 +154,9 @@ class CustomColumns(object): tn = 'custom_column_{0}'.format(v['num']) self.field_metadata.add_custom_field(label=v['label'], table=tn, column='value', datatype=v['datatype'], - is_multiple=is_m, colnum=v['num'], name=v['name'], - is_category=is_category) + colnum=v['num'], name=v['name'], display=v['display'], + is_multiple=is_m, is_category=is_category, + is_editable=v['editable']) def get_custom(self, idx, label=None, num=None, index_is_id=False): if label is not None: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 5d4c2a783e..5ba603cc52 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -232,8 +232,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.commit() self.book_on_device_func = None - self.data = ResultCache(self.FIELD_MAP, self.custom_column_label_map, - self.field_metadata) + self.data = ResultCache(self.FIELD_MAP, self.field_metadata) self.search = self.data.search self.refresh = functools.partial(self.data.refresh, self) self.sort = self.data.sort diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 638b7c7dd0..6f1219ad03 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -282,7 +282,9 @@ class FieldMetadata(dict): self.custom_label_to_key_map = {} for k,v in self._field_metadata: self._tb_cats[k] = v - self._tb_cats[k]['label'] = k # saved some typing above... + self._tb_cats[k]['label'] = k + self._tb_cats[k]['display'] = {} + self._tb_cats[k]['is_editable'] = True self._add_search_terms_to_map(k, self._tb_cats[k]['search_terms']) self.custom_field_prefix = '#' self.get = self._tb_cats.get @@ -300,12 +302,12 @@ class FieldMetadata(dict): for key in self._tb_cats: yield key - def has_key(self, key): - return key in self._tb_cats - def __contains__(self, key): return self.has_key(key) + def has_key(self, key): + return key in self._tb_cats + def keys(self): return self._tb_cats.keys() @@ -339,8 +341,15 @@ class FieldMetadata(dict): def get_custom_fields(self): return [l for l in self._tb_cats if self._tb_cats[l]['is_custom']] - def add_custom_field(self, label, table, column, datatype, colnum, - name, is_multiple, is_category): + def get_custom_field_metadata(self): + l = {} + for k in self._tb_cats: + if self._tb_cats[k]['is_custom']: + l[k] = self._tb_cats[k] + return l + + def add_custom_field(self, label, table, column, datatype, colnum, name, + display, is_editable, is_multiple, is_category): key = self.custom_field_prefix + label if key in self._tb_cats: raise ValueError('Duplicate custom field [%s]'%(label)) @@ -348,8 +357,9 @@ class FieldMetadata(dict): 'datatype':datatype, 'is_multiple':is_multiple, 'kind':'field', 'name':name, 'search_terms':[key], 'label':label, - 'colnum':colnum, 'is_custom':True, - 'is_category':is_category} + 'colnum':colnum, 'display':display, + 'is_custom':True, 'is_category':is_category, + 'is_editable': is_editable,} self._add_search_terms_to_map(key, [key]) self.custom_label_to_key_map[label] = key