mirror of
https://github.com/gryf/ebook-converter.git
synced 2026-01-02 00:22:25 +01:00
700 lines
30 KiB
Python
700 lines
30 KiB
Python
"""
|
|
Created on 25 May 2010
|
|
|
|
@author: charles
|
|
"""
|
|
import traceback
|
|
from collections import OrderedDict
|
|
|
|
from ebook_converter.utils.config_base import tweaks
|
|
from ebook_converter.polyglot.builtins import iteritems, itervalues
|
|
|
|
category_icon_map = {
|
|
'authors' : 'user_profile.png',
|
|
'series' : 'series.png',
|
|
'formats' : 'book.png',
|
|
'publisher' : 'publisher.png',
|
|
'rating' : 'rating.png',
|
|
'news' : 'news.png',
|
|
'tags' : 'tags.png',
|
|
'custom:' : 'column.png',
|
|
'user:' : 'tb_folder.png',
|
|
'search' : 'search.png',
|
|
'identifiers': 'identifiers.png',
|
|
'gst' : 'catalog.png',
|
|
'languages' : 'languages.png',
|
|
}
|
|
|
|
# Builtin metadata {{{
|
|
|
|
|
|
def _builtin_field_metadata():
|
|
# This is a function so that changing the UI language allows newly created
|
|
# field metadata objects to have correctly translated labels for builtin
|
|
# fields.
|
|
return [
|
|
('authors', {'table':'authors',
|
|
'column':'name',
|
|
'link_column':'author',
|
|
'category_sort':'sort',
|
|
'datatype':'text',
|
|
'is_multiple':{'cache_to_list': ',',
|
|
'ui_to_list': '&',
|
|
'list_to_ui': ' & '},
|
|
'kind':'field',
|
|
'name':_('Authors'),
|
|
'search_terms':['authors', 'author'],
|
|
'is_custom':False,
|
|
'is_category':True,
|
|
'is_csp': False}),
|
|
('languages', {'table':'languages',
|
|
'column':'lang_code',
|
|
'link_column':'lang_code',
|
|
'category_sort':'lang_code',
|
|
'datatype':'text',
|
|
'is_multiple':{'cache_to_list': ',',
|
|
'ui_to_list': ',',
|
|
'list_to_ui': ', '},
|
|
'kind':'field',
|
|
'name':_('Languages'),
|
|
'search_terms':['languages', 'language'],
|
|
'is_custom':False,
|
|
'is_category':True,
|
|
'is_csp': False}),
|
|
|
|
('series', {'table':'series',
|
|
'column':'name',
|
|
'link_column':'series',
|
|
'category_sort':'(title_sort(name))',
|
|
'datatype':'series',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':ngettext('Series', 'Series', 1),
|
|
'search_terms':['series'],
|
|
'is_custom':False,
|
|
'is_category':True,
|
|
'is_csp': False}),
|
|
('formats', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{'cache_to_list': ',',
|
|
'ui_to_list': ',',
|
|
'list_to_ui': ', '},
|
|
'kind':'field',
|
|
'name':_('Formats'),
|
|
'search_terms':['formats', 'format'],
|
|
'is_custom':False,
|
|
'is_category':True,
|
|
'is_csp': False}),
|
|
('publisher', {'table':'publishers',
|
|
'column':'name',
|
|
'link_column':'publisher',
|
|
'category_sort':'name',
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Publisher'),
|
|
'search_terms':['publisher'],
|
|
'is_custom':False,
|
|
'is_category':True,
|
|
'is_csp': False}),
|
|
('rating', {'table':'ratings',
|
|
'column':'rating',
|
|
'link_column':'rating',
|
|
'category_sort':'rating',
|
|
'datatype':'rating',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Rating'),
|
|
'search_terms':['rating'],
|
|
'is_custom':False,
|
|
'is_category':True,
|
|
'is_csp': False}),
|
|
('news', {'table':'news',
|
|
'column':'name',
|
|
'category_sort':'name',
|
|
'datatype':None,
|
|
'is_multiple':{},
|
|
'kind':'category',
|
|
'name':_('News'),
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':True,
|
|
'is_csp': False}),
|
|
('tags', {'table':'tags',
|
|
'column':'name',
|
|
'link_column': 'tag',
|
|
'category_sort':'name',
|
|
'datatype':'text',
|
|
'is_multiple':{'cache_to_list': ',',
|
|
'ui_to_list': ',',
|
|
'list_to_ui': ', '},
|
|
'kind':'field',
|
|
'name':_('Tags'),
|
|
'search_terms':['tags', 'tag'],
|
|
'is_custom':False,
|
|
'is_category':True,
|
|
'is_csp': False}),
|
|
('identifiers', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{'cache_to_list': ',',
|
|
'ui_to_list': ',',
|
|
'list_to_ui': ', '},
|
|
'kind':'field',
|
|
'name':_('Identifiers'),
|
|
'search_terms':['identifiers', 'identifier', 'isbn'],
|
|
'is_custom':False,
|
|
'is_category':True,
|
|
'is_csp': True}),
|
|
('author_sort',{'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Author sort'),
|
|
'search_terms':['author_sort'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('au_map', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{'cache_to_list': ',',
|
|
'ui_to_list': None,
|
|
'list_to_ui': None},
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('comments', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Comments'),
|
|
'search_terms':['comments', 'comment'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('cover', {'table':None,
|
|
'column':None,
|
|
'datatype':'int',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Cover'),
|
|
'search_terms':['cover'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('id', {'table':None,
|
|
'column':None,
|
|
'datatype':'int',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':['id'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('last_modified', {'table':None,
|
|
'column':None,
|
|
'datatype':'datetime',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Modified'),
|
|
'search_terms':['last_modified'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('ondevice', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('On device'),
|
|
'search_terms':['ondevice'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('path', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Path'),
|
|
'search_terms':[],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('pubdate', {'table':None,
|
|
'column':None,
|
|
'datatype':'datetime',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Published'),
|
|
'search_terms':['pubdate'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('marked', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name': None,
|
|
'search_terms':['marked'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('series_index',{'table':None,
|
|
'column':None,
|
|
'datatype':'float',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':['series_index'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('series_sort', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Series sort'),
|
|
'search_terms':['series_sort'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('sort', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Title sort'),
|
|
'search_terms':['title_sort'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('size', {'table':None,
|
|
'column':None,
|
|
'datatype':'float',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Size'),
|
|
'search_terms':['size'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('timestamp', {'table':None,
|
|
'column':None,
|
|
'datatype':'datetime',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Date'),
|
|
'search_terms':['date'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('title', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':_('Title'),
|
|
'search_terms':['title'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
('uuid', {'table':None,
|
|
'column':None,
|
|
'datatype':'text',
|
|
'is_multiple':{},
|
|
'kind':'field',
|
|
'name':None,
|
|
'search_terms':['uuid'],
|
|
'is_custom':False,
|
|
'is_category':False,
|
|
'is_csp': False}),
|
|
]
|
|
# }}}
|
|
|
|
|
|
class FieldMetadata(object):
|
|
'''
|
|
key: the key to the dictionary is:
|
|
- for standard fields, the metadata field name.
|
|
- for custom fields, the metadata field name prefixed by '#'
|
|
This is done to create two 'namespaces' so the names don't clash
|
|
|
|
label: the actual column label. No prefixing.
|
|
|
|
datatype: the type of information in the field. Valid values are listed in
|
|
VALID_DATA_TYPES below.
|
|
is_multiple: valid for the text datatype. If {}, the field is to be
|
|
treated as a single term. If not None, it contains a dict of the form
|
|
{'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 == category: standard tag category that isn't a field. see news.
|
|
kind == user: user-defined tag category.
|
|
kind == search: saved-searches category.
|
|
|
|
is_category: is a tag browser category. If true, then:
|
|
table: name of the db table used to construct item list
|
|
column: name of the column in the normalized table to join on
|
|
link_column: name of the column in the connection table to join on. This
|
|
key should not be present if there is no link table
|
|
category_sort: the field in the normalized table to sort on. This
|
|
key must be present if is_category is True
|
|
If these are None, then the category constructor must know how
|
|
to build the item list (e.g., formats, news).
|
|
The order below is the order that the categories will
|
|
appear in the tags pane.
|
|
|
|
name: the text that is to be used when displaying the field. Column headings
|
|
in the GUI, etc.
|
|
|
|
search_terms: the terms that can be used to identify the field when
|
|
searching. They can be thought of as aliases for metadata keys, but are only
|
|
valid when passed to search().
|
|
|
|
is_custom: the field has been added by the user.
|
|
|
|
rec_index: the index of the field in the db metadata record.
|
|
|
|
is_csp: field contains colon-separated pairs. Must also be text, is_multiple
|
|
|
|
'''
|
|
|
|
VALID_DATA_TYPES = frozenset([None, 'rating', 'text', 'comments', 'datetime',
|
|
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
|
|
|
|
# search labels that are not db columns
|
|
search_items = ['all', 'search', 'vl']
|
|
__calibre_serializable__ = True
|
|
|
|
def __init__(self):
|
|
self._field_metadata = _builtin_field_metadata()
|
|
self._tb_cats = OrderedDict()
|
|
self._tb_custom_fields = {}
|
|
self._search_term_map = {}
|
|
self.custom_label_to_key_map = {}
|
|
for k,v in self._field_metadata:
|
|
if v['kind'] == 'field' and v['datatype'] not in self.VALID_DATA_TYPES:
|
|
raise ValueError('Unknown datatype %s for field %s'%(v['datatype'], k))
|
|
self._tb_cats[k] = v
|
|
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, v['search_terms'])
|
|
self._tb_cats['timestamp']['display'] = {
|
|
'date_format': tweaks['gui_timestamp_display_format']}
|
|
self._tb_cats['pubdate']['display'] = {
|
|
'date_format': tweaks['gui_pubdate_display_format']}
|
|
self._tb_cats['last_modified']['display'] = {
|
|
'date_format': tweaks['gui_last_modified_display_format']}
|
|
self.custom_field_prefix = '#'
|
|
self.get = self._tb_cats.get
|
|
|
|
def __getitem__(self, key):
|
|
if key == 'title_sort':
|
|
return self._tb_cats['sort']
|
|
return self._tb_cats[key]
|
|
|
|
def __setitem__(self, key, val):
|
|
raise AttributeError('Assigning to this object is forbidden')
|
|
|
|
def __delitem__(self, key):
|
|
del self._tb_cats[key]
|
|
|
|
def __iter__(self):
|
|
for key in self._tb_cats:
|
|
yield key
|
|
|
|
def __contains__(self, key):
|
|
return key in self._tb_cats or key == 'title_sort'
|
|
|
|
def has_key(self, key):
|
|
return key in self
|
|
|
|
def keys(self):
|
|
return list(self._tb_cats.keys())
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, FieldMetadata):
|
|
return False
|
|
for attr in ('_tb_custom_fields', '_search_term_map', 'custom_label_to_key_map', 'custom_field_prefix'):
|
|
if getattr(self, attr) != getattr(other, attr):
|
|
return False
|
|
return dict(self._tb_cats) == dict(other._tb_cats)
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def sortable_field_keys(self):
|
|
return [k for k in self._tb_cats.keys()
|
|
if self._tb_cats[k]['kind']=='field' and
|
|
self._tb_cats[k]['datatype'] is not None]
|
|
|
|
def ui_sortable_field_keys(self):
|
|
ans = {k:self._tb_cats[k]['name'] for k in set(self.sortable_field_keys()) - {
|
|
'sort', 'author_sort', 'au_map', 'series_sort', 'marked',
|
|
'series_index', 'path', 'formats', 'identifiers', 'uuid',
|
|
'comments',
|
|
} if self._tb_cats[k]['name']}
|
|
ans['cover'] = _('Has cover')
|
|
return ans
|
|
|
|
def displayable_field_keys(self):
|
|
return [k for k in self._tb_cats.keys()
|
|
if self._tb_cats[k]['kind']=='field' and
|
|
self._tb_cats[k]['datatype'] is not None and
|
|
k not in ('au_map', 'marked', 'ondevice', 'cover', 'series_sort') and
|
|
not self.is_series_index(k)]
|
|
|
|
def standard_field_keys(self):
|
|
return [k for k in self._tb_cats.keys()
|
|
if self._tb_cats[k]['kind']=='field' and
|
|
not self._tb_cats[k]['is_custom']]
|
|
|
|
def custom_field_keys(self, include_composites=True):
|
|
res = []
|
|
for k in self._tb_cats.keys():
|
|
fm = self._tb_cats[k]
|
|
if fm['kind']=='field' and fm['is_custom'] and \
|
|
(fm['datatype'] != 'composite' or include_composites):
|
|
res.append(k)
|
|
return res
|
|
|
|
def all_field_keys(self):
|
|
return [k for k in self._tb_cats.keys() if self._tb_cats[k]['kind']=='field']
|
|
|
|
def iterkeys(self):
|
|
for key in self._tb_cats:
|
|
yield key
|
|
|
|
def itervalues(self):
|
|
return itervalues(self._tb_cats)
|
|
|
|
def values(self):
|
|
return list(self._tb_cats.values())
|
|
|
|
def iteritems(self):
|
|
for key in self._tb_cats:
|
|
yield (key, self._tb_cats[key])
|
|
iter_items = iteritems
|
|
|
|
def custom_iteritems(self):
|
|
for key, meta in iteritems(self._tb_custom_fields):
|
|
yield (key, meta)
|
|
|
|
def items(self):
|
|
return list(self.iter_items())
|
|
|
|
def is_custom_field(self, key):
|
|
return key.startswith(self.custom_field_prefix)
|
|
|
|
def is_ignorable_field(self, key):
|
|
'Custom fields and user categories are ignorable'
|
|
return self.is_custom_field(key) or key.startswith('@')
|
|
|
|
def ignorable_field_keys(self):
|
|
return [k for k in self._tb_cats if self.is_ignorable_field(k)]
|
|
|
|
def is_series_index(self, key):
|
|
try:
|
|
m = self._tb_cats[key]
|
|
return (m['datatype'] == 'float' and key.endswith('_index') and
|
|
key[:-6] in self._tb_cats)
|
|
except (KeyError, ValueError, TypeError, AttributeError):
|
|
return False
|
|
|
|
def key_to_label(self, key):
|
|
if 'label' not in self._tb_cats[key]:
|
|
return key
|
|
return self._tb_cats[key]['label']
|
|
|
|
def label_to_key(self, label, prefer_custom=False):
|
|
if prefer_custom:
|
|
if label in self.custom_label_to_key_map:
|
|
return self.custom_label_to_key_map[label]
|
|
if 'label' in self._tb_cats:
|
|
return label
|
|
if not prefer_custom:
|
|
if label in self.custom_label_to_key_map:
|
|
return self.custom_label_to_key_map[label]
|
|
raise ValueError('Unknown key [%s]'%(label))
|
|
|
|
def all_metadata(self):
|
|
l = {}
|
|
for k in self._tb_cats:
|
|
l[k] = self._tb_cats[k]
|
|
return l
|
|
|
|
def custom_field_metadata(self, include_composites=True):
|
|
if include_composites:
|
|
return self._tb_custom_fields
|
|
l = {}
|
|
for k in self.custom_field_keys(include_composites):
|
|
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,
|
|
is_csp=False):
|
|
key = self.custom_field_prefix + label
|
|
if key in self._tb_cats:
|
|
raise ValueError('Duplicate custom field [%s]'%(label))
|
|
if datatype not in self.VALID_DATA_TYPES:
|
|
raise ValueError('Unknown datatype %s for field %s'%(datatype, key))
|
|
self._tb_cats[key] = {'table':table, 'column':column,
|
|
'datatype':datatype, 'is_multiple':is_multiple,
|
|
'kind':'field', 'name':name,
|
|
'search_terms':[key], 'label':label,
|
|
'colnum':colnum, 'display':display,
|
|
'is_custom':True, 'is_category':is_category,
|
|
'link_column':'value','category_sort':'value',
|
|
'is_csp' : is_csp, 'is_editable': is_editable,}
|
|
self._tb_custom_fields[key] = self._tb_cats[key]
|
|
self._add_search_terms_to_map(key, [key])
|
|
self.custom_label_to_key_map[label] = key
|
|
if datatype == 'series':
|
|
key += '_index'
|
|
self._tb_cats[key] = {'table':None, 'column':None,
|
|
'datatype':'float', 'is_multiple':{},
|
|
'kind':'field', 'name':'',
|
|
'search_terms':[key], 'label':label+'_index',
|
|
'colnum':None, 'display':{},
|
|
'is_custom':False, 'is_category':False,
|
|
'link_column':None, 'category_sort':None,
|
|
'is_editable': False, 'is_csp': False}
|
|
self._add_search_terms_to_map(key, [key])
|
|
self.custom_label_to_key_map[label+'_index'] = key
|
|
|
|
def remove_dynamic_categories(self):
|
|
for key in list(self._tb_cats.keys()):
|
|
val = self._tb_cats[key]
|
|
if val['is_category'] and val['kind'] in ('user', 'search'):
|
|
for k in self._tb_cats[key]['search_terms']:
|
|
if k in self._search_term_map:
|
|
del self._search_term_map[k]
|
|
del self._tb_cats[key]
|
|
|
|
def remove_user_categories(self):
|
|
for key in list(self._tb_cats.keys()):
|
|
val = self._tb_cats[key]
|
|
if val['is_category'] and val['kind'] == 'user':
|
|
for k in self._tb_cats[key]['search_terms']:
|
|
if k in self._search_term_map:
|
|
del self._search_term_map[k]
|
|
del self._tb_cats[key]
|
|
|
|
def _remove_grouped_search_terms(self):
|
|
to_remove = [v for v in self._search_term_map
|
|
if isinstance(self._search_term_map[v], list)]
|
|
for v in to_remove:
|
|
del self._search_term_map[v]
|
|
|
|
def add_grouped_search_terms(self, gst):
|
|
self._remove_grouped_search_terms()
|
|
for t in gst:
|
|
try:
|
|
self._add_search_terms_to_map(gst[t], [t])
|
|
except ValueError:
|
|
traceback.print_exc()
|
|
|
|
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:
|
|
raise ValueError('Duplicate user field [%s]'%(label))
|
|
st = [label]
|
|
if icu_lower(label) != label:
|
|
st.append(icu_lower(label))
|
|
self._tb_cats[label] = {'table':None, 'column':None,
|
|
'datatype':None, 'is_multiple':{},
|
|
'kind':'user', 'name':name,
|
|
'search_terms':st, 'is_custom':False,
|
|
'is_category':True, 'is_csp': False}
|
|
self._add_search_terms_to_map(label, st)
|
|
|
|
def add_search_category(self, label, name, fail_on_existing=True):
|
|
if label in self._tb_cats:
|
|
if not fail_on_existing:
|
|
return
|
|
raise ValueError('Duplicate user field [%s]'%(label))
|
|
self._tb_cats[label] = {'table':None, 'column':None,
|
|
'datatype':None, 'is_multiple':{},
|
|
'kind':'search', 'name':name,
|
|
'search_terms':[], 'is_custom':False,
|
|
'is_category':True, 'is_csp': False}
|
|
|
|
def set_field_record_index(self, label, index, prefer_custom=False):
|
|
if prefer_custom:
|
|
key = self.custom_field_prefix+label
|
|
if key not in self._tb_cats:
|
|
key = label
|
|
else:
|
|
if label in self._tb_cats:
|
|
key = label
|
|
else:
|
|
key = self.custom_field_prefix+label
|
|
self._tb_cats[key]['rec_index'] = index # let the exception fly ...
|
|
|
|
def get_search_terms(self):
|
|
s_keys = sorted(self._search_term_map.keys())
|
|
for v in self.search_items:
|
|
s_keys.append(v)
|
|
return s_keys
|
|
|
|
def _add_search_terms_to_map(self, key, terms):
|
|
if terms is not None:
|
|
for t in terms:
|
|
if t in self._search_term_map:
|
|
raise ValueError('Attempt to add duplicate search term "%s"'%t)
|
|
self._search_term_map[t] = key
|
|
|
|
def search_term_to_field_key(self, term):
|
|
return self._search_term_map.get(term, term)
|
|
|
|
def searchable_fields(self):
|
|
return [k for k in self._tb_cats.keys()
|
|
if self._tb_cats[k]['kind']=='field' and
|
|
len(self._tb_cats[k]['search_terms']) > 0]
|
|
|
|
|
|
# The following two methods are to support serialization
|
|
# Note that they do not create copies of internal structures, for performance,
|
|
# so they are not safe to use for anything else
|
|
def fm_as_dict(self):
|
|
return {
|
|
'custom_fields': self._tb_custom_fields,
|
|
'search_term_map': self._search_term_map,
|
|
'custom_label_to_key_map': self.custom_label_to_key_map,
|
|
'user_categories': {k:v for k, v in iteritems(self._tb_cats) if v['kind'] == 'user'},
|
|
'search_categories': {k:v for k, v in iteritems(self._tb_cats) if v['kind'] == 'search'},
|
|
}
|
|
|
|
|
|
def fm_from_dict(src):
|
|
ans = FieldMetadata()
|
|
ans._tb_custom_fields = src['custom_fields']
|
|
ans._search_term_map = src['search_term_map']
|
|
ans.custom_label_to_key_map = src['custom_label_to_key_map']
|
|
for q in ('custom_fields', 'user_categories', 'search_categories'):
|
|
for k, v in iteritems(src[q]):
|
|
ans._tb_cats[k] = v
|
|
return ans
|