mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
API for creating, modifying and deleting custom columns
This commit is contained in:
parent
de5237c4d5
commit
3cc7a7374d
@ -42,6 +42,8 @@ Differences in semantics from pysqlite:
|
|||||||
3. There is no executescript
|
3. There is no executescript
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
|
||||||
|
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
|
||||||
|
|
||||||
|
|
||||||
class DynamicFilter(object): # {{{
|
class DynamicFilter(object): # {{{
|
||||||
@ -797,6 +799,184 @@ class DB(object):
|
|||||||
return self.custom_column_label_map[label]
|
return self.custom_column_label_map[label]
|
||||||
return self.custom_column_num_map[num]
|
return self.custom_column_num_map[num]
|
||||||
|
|
||||||
|
def set_custom_column_metadata(self, num, name=None, label=None, is_editable=None, display=None):
|
||||||
|
changed = False
|
||||||
|
if name is not None:
|
||||||
|
self.conn.execute('UPDATE custom_columns SET name=? WHERE id=?', (name, num))
|
||||||
|
changed = True
|
||||||
|
if label is not None:
|
||||||
|
self.conn.execute('UPDATE custom_columns SET label=? WHERE id=?', (label, num))
|
||||||
|
changed = True
|
||||||
|
if is_editable is not None:
|
||||||
|
self.conn.execute('UPDATE custom_columns SET editable=? WHERE id=?', (bool(is_editable), num))
|
||||||
|
self.custom_column_num_map[num]['is_editable'] = bool(is_editable)
|
||||||
|
changed = True
|
||||||
|
if display is not None:
|
||||||
|
self.conn.execute('UPDATE custom_columns SET display=? WHERE id=?', (json.dumps(display), num))
|
||||||
|
changed = True
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def create_custom_column(self, label, name, datatype, is_multiple, editable=True, display={}): # {{{
|
||||||
|
import re
|
||||||
|
if not label:
|
||||||
|
raise ValueError(_('No label was provided'))
|
||||||
|
if re.match('^\w*$', label) is None or not label[0].isalpha() or label.lower() != label:
|
||||||
|
raise ValueError(_('The label must contain only lower case letters, digits and underscores, and start with a letter'))
|
||||||
|
if datatype not in CUSTOM_DATA_TYPES:
|
||||||
|
raise ValueError('%r is not a supported data type'%datatype)
|
||||||
|
normalized = datatype not in ('datetime', 'comments', 'int', 'bool',
|
||||||
|
'float', 'composite')
|
||||||
|
is_multiple = is_multiple and datatype in ('text', 'composite')
|
||||||
|
self.conn.execute(
|
||||||
|
('INSERT INTO '
|
||||||
|
'custom_columns(label,name,datatype,is_multiple,editable,display,normalized)'
|
||||||
|
'VALUES (?,?,?,?,?,?,?)'),
|
||||||
|
(label, name, datatype, is_multiple, editable, json.dumps(display), normalized))
|
||||||
|
num = self.conn.last_insert_rowid()
|
||||||
|
|
||||||
|
if datatype in ('rating', 'int'):
|
||||||
|
dt = 'INT'
|
||||||
|
elif datatype in ('text', 'comments', 'series', 'composite', 'enumeration'):
|
||||||
|
dt = 'TEXT'
|
||||||
|
elif datatype in ('float',):
|
||||||
|
dt = 'REAL'
|
||||||
|
elif datatype == 'datetime':
|
||||||
|
dt = 'timestamp'
|
||||||
|
elif datatype == 'bool':
|
||||||
|
dt = 'BOOL'
|
||||||
|
collate = 'COLLATE NOCASE' if dt == 'TEXT' else ''
|
||||||
|
table, lt = self.custom_table_names(num)
|
||||||
|
if normalized:
|
||||||
|
if datatype == 'series':
|
||||||
|
s_index = 'extra REAL,'
|
||||||
|
else:
|
||||||
|
s_index = ''
|
||||||
|
lines = [
|
||||||
|
'''\
|
||||||
|
CREATE TABLE %s(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
value %s NOT NULL %s,
|
||||||
|
UNIQUE(value));
|
||||||
|
'''%(table, dt, collate),
|
||||||
|
|
||||||
|
'CREATE INDEX %s_idx ON %s (value %s);'%(table, table, collate),
|
||||||
|
|
||||||
|
'''\
|
||||||
|
CREATE TABLE %s(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
book INTEGER NOT NULL,
|
||||||
|
value INTEGER NOT NULL,
|
||||||
|
%s
|
||||||
|
UNIQUE(book, value)
|
||||||
|
);'''%(lt, s_index),
|
||||||
|
|
||||||
|
'CREATE INDEX %s_aidx ON %s (value);'%(lt,lt),
|
||||||
|
'CREATE INDEX %s_bidx ON %s (book);'%(lt,lt),
|
||||||
|
|
||||||
|
'''\
|
||||||
|
CREATE TRIGGER fkc_update_{lt}_a
|
||||||
|
BEFORE UPDATE OF book ON {lt}
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_{lt}_b
|
||||||
|
BEFORE UPDATE OF author ON {lt}
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from {table} WHERE id=NEW.value) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: value not in {table}')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_insert_{lt}
|
||||||
|
BEFORE INSERT ON {lt}
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
WHEN (SELECT id from {table} WHERE id=NEW.value) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: value not in {table}')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_delete_{lt}
|
||||||
|
AFTER DELETE ON {table}
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM {lt} WHERE value=OLD.id;
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE VIEW tag_browser_{table} AS SELECT
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
|
||||||
|
(SELECT AVG(r.rating)
|
||||||
|
FROM {lt},
|
||||||
|
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,
|
||||||
|
value AS sort
|
||||||
|
FROM {table};
|
||||||
|
|
||||||
|
CREATE VIEW tag_browser_filtered_{table} AS SELECT
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
|
||||||
|
books_list_filter(book)) count,
|
||||||
|
(SELECT AVG(r.rating)
|
||||||
|
FROM {lt},
|
||||||
|
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 AND
|
||||||
|
books_list_filter(bl.book)) avg_rating,
|
||||||
|
value AS sort
|
||||||
|
FROM {table};
|
||||||
|
|
||||||
|
'''.format(lt=lt, table=table),
|
||||||
|
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
lines = [
|
||||||
|
'''\
|
||||||
|
CREATE TABLE %s(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
book INTEGER,
|
||||||
|
value %s NOT NULL %s,
|
||||||
|
UNIQUE(book));
|
||||||
|
'''%(table, dt, collate),
|
||||||
|
|
||||||
|
'CREATE INDEX %s_idx ON %s (book);'%(table, table),
|
||||||
|
|
||||||
|
'''\
|
||||||
|
CREATE TRIGGER fkc_insert_{table}
|
||||||
|
BEFORE INSERT ON {table}
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
CREATE TRIGGER fkc_update_{table}
|
||||||
|
BEFORE UPDATE OF book ON {table}
|
||||||
|
BEGIN
|
||||||
|
SELECT CASE
|
||||||
|
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
|
||||||
|
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
'''.format(table=table),
|
||||||
|
]
|
||||||
|
script = ' \n'.join(lines)
|
||||||
|
self.conn.execute(script)
|
||||||
|
return num
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def delete_custom_column(self, label=None, num=None):
|
||||||
|
data = self.custom_field_metadata(label, num)
|
||||||
|
self.conn.execute('UPDATE custom_columns SET mark_for_delete=1 WHERE id=?', (data['num'],))
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self._conn is not None:
|
if self._conn is not None:
|
||||||
self._conn.close()
|
self._conn.close()
|
||||||
@ -1274,3 +1454,4 @@ class DB(object):
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1401,6 +1401,17 @@ class Cache(object):
|
|||||||
def lookup_by_uuid(self, uuid):
|
def lookup_by_uuid(self, uuid):
|
||||||
return self.fields['uuid'].table.lookup_by_uuid(uuid)
|
return self.fields['uuid'].table.lookup_by_uuid(uuid)
|
||||||
|
|
||||||
|
@write_api
|
||||||
|
def delete_custom_column(self, label=None, num=None):
|
||||||
|
self.backend.delete_custom_column(label, num)
|
||||||
|
|
||||||
|
@write_api
|
||||||
|
def create_custom_column(self, label, name, datatype, is_multiple, editable=True, display={}):
|
||||||
|
self.backend.create_custom_column(label, name, datatype, is_multiple, editable=editable, display=display)
|
||||||
|
|
||||||
|
@write_api
|
||||||
|
def set_custom_column_metadata(self, num, name=None, label=None, is_editable=None, display=None):
|
||||||
|
return self.backend.set_custom_column_metadata(num, name=name, label=label, is_editable=is_editable, display=display)
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -653,6 +653,17 @@ class LibraryDatabase(object):
|
|||||||
if notify:
|
if notify:
|
||||||
self.notify('metadata', list(ids))
|
self.notify('metadata', list(ids))
|
||||||
|
|
||||||
|
def delete_custom_column(self, label=None, num=None):
|
||||||
|
self.new_api.delete_custom_column(label, num)
|
||||||
|
|
||||||
|
def create_custom_column(self, label, name, datatype, is_multiple, editable=True, display={}):
|
||||||
|
self.new_api.create_custom_column(label, name, datatype, is_multiple, editable=editable, display=display)
|
||||||
|
|
||||||
|
def set_custom_column_metadata(self, num, name=None, label=None, is_editable=None, display=None, notify=True):
|
||||||
|
changed = self.new_api.set_custom_column_metadata(num, name=name, label=label, is_editable=is_editable, display=display)
|
||||||
|
if changed and notify:
|
||||||
|
self.notify('metadata', [])
|
||||||
|
|
||||||
# Private interface {{{
|
# Private interface {{{
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for row in self.data.iterall():
|
for row in self.data.iterall():
|
||||||
@ -843,3 +854,4 @@ del MT
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -689,5 +689,25 @@ class LegacyTest(BaseTest):
|
|||||||
('get_custom', 0, 'tags'), ('get_custom', 1, 'tags'), ('get_custom', 2, 'tags'),
|
('get_custom', 0, 'tags'), ('get_custom', 1, 'tags'), ('get_custom', 2, 'tags'),
|
||||||
))
|
))
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
o = self.cloned_library
|
||||||
|
n = self.cloned_library
|
||||||
|
ndb, db = self.init_legacy(n), self.init_old(o)
|
||||||
|
ndb.create_custom_column('created', 'Created', 'text', True, True, {'moose':'cat'})
|
||||||
|
db.create_custom_column('created', 'Created', 'text', True, True, {'moose':'cat'})
|
||||||
|
db.close()
|
||||||
|
ndb, db = self.init_legacy(n), self.init_old(o)
|
||||||
|
self.assertEqual(db.custom_column_label_map['created'], ndb.backend.custom_field_metadata('created'))
|
||||||
|
num = db.custom_column_label_map['created']['num']
|
||||||
|
ndb.set_custom_column_metadata(num, is_editable=False, name='Crikey', display={})
|
||||||
|
db.set_custom_column_metadata(num, is_editable=False, name='Crikey', display={})
|
||||||
|
db.close()
|
||||||
|
ndb, db = self.init_legacy(n), self.init_old(o)
|
||||||
|
self.assertEqual(db.custom_column_label_map['created'], ndb.backend.custom_field_metadata('created'))
|
||||||
|
db.close()
|
||||||
|
ndb = self.init_legacy(n)
|
||||||
|
ndb.delete_custom_column('created')
|
||||||
|
ndb = self.init_legacy(n)
|
||||||
|
self.assertRaises(KeyError, ndb.custom_field_name, num=num)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user