API for creating, modifying and deleting custom columns

This commit is contained in:
Kovid Goyal 2013-07-17 17:40:45 +05:30
parent de5237c4d5
commit 3cc7a7374d
4 changed files with 224 additions and 0 deletions

View File

@ -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):
# }}}

View File

@ -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)
# }}}

View File

@ -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

View File

@ -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)
# }}}