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
|
||||
|
||||
'''
|
||||
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
|
||||
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
|
||||
|
||||
|
||||
class DynamicFilter(object): # {{{
|
||||
@ -797,6 +799,184 @@ class DB(object):
|
||||
return self.custom_column_label_map[label]
|
||||
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):
|
||||
if self._conn is not None:
|
||||
self._conn.close()
|
||||
@ -1274,3 +1454,4 @@ class DB(object):
|
||||
|
||||
# }}}
|
||||
|
||||
|
||||
|
@ -1401,6 +1401,17 @@ class Cache(object):
|
||||
def lookup_by_uuid(self, 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:
|
||||
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 {{{
|
||||
def __iter__(self):
|
||||
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'),
|
||||
))
|
||||
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