New db: Implement sorting for all field types. Also allow use of field_for() with composite columns

This commit is contained in:
Kovid Goyal 2011-07-15 15:14:12 -06:00
parent 5c9c46382f
commit 2290a06894
2 changed files with 82 additions and 7 deletions

View File

@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
import os import os
from collections import defaultdict from collections import defaultdict
from functools import wraps from functools import wraps, partial
from calibre.db.locking import create_locks, RecordLock from calibre.db.locking import create_locks, RecordLock
from calibre.db.fields import create_field from calibre.db.fields import create_field
@ -43,6 +43,7 @@ class Cache(object):
def __init__(self, backend): def __init__(self, backend):
self.backend = backend self.backend = backend
self.fields = {} self.fields = {}
self.composites = set()
self.read_lock, self.write_lock = create_locks() self.read_lock, self.write_lock = create_locks()
self.record_lock = RecordLock(self.read_lock) self.record_lock = RecordLock(self.read_lock)
self.format_metadata_cache = defaultdict(dict) self.format_metadata_cache = defaultdict(dict)
@ -82,7 +83,7 @@ class Cache(object):
if name and path: if name and path:
return self.backend.format_abspath(book_id, fmt, name, path) return self.backend.format_abspath(book_id, fmt, name, path)
def _get_metadata(self, book_id, get_user_categories=True): def _get_metadata(self, book_id, get_user_categories=True): # {{{
mi = Metadata(None) mi = Metadata(None)
author_ids = self._field_ids_for('authors', book_id) author_ids = self._field_ids_for('authors', book_id)
aut_list = [self._author_data(i) for i in author_ids] aut_list = [self._author_data(i) for i in author_ids]
@ -162,6 +163,7 @@ class Cache(object):
mi.user_categories = user_cat_vals mi.user_categories = user_cat_vals
return mi return mi
# }}}
# Cache Layer API {{{ # Cache Layer API {{{
@ -175,6 +177,8 @@ class Cache(object):
for field, table in self.backend.tables.iteritems(): for field, table in self.backend.tables.iteritems():
self.fields[field] = create_field(field, table) self.fields[field] = create_field(field, table)
if table.metadata['datatype'] == 'composite':
self.composites.add(field)
self.fields['ondevice'] = create_field('ondevice', None) self.fields['ondevice'] = create_field('ondevice', None)
@ -187,19 +191,26 @@ class Cache(object):
The returned value for is_multiple fields are always tuples. The returned value for is_multiple fields are always tuples.
''' '''
if self.composites and name in self.composites:
return self.composite_for(name, book_id,
default_value=default_value)
try: try:
return self.fields[name].for_book(book_id, default_value=default_value) return self.fields[name].for_book(book_id, default_value=default_value)
except (KeyError, IndexError): except (KeyError, IndexError):
return default_value return default_value
@read_api @read_api
def composite_for(self, name, book_id, mi, default_value=''): def composite_for(self, name, book_id, mi=None, default_value=''):
try: try:
f = self.fields[name] f = self.fields[name]
except KeyError: except KeyError:
return default_value return default_value
f.render_composite(book_id, mi) if mi is None:
return f.get_value_with_cache(book_id, partial(self._get_metadata,
get_user_categories=False))
else:
return f.render_composite(book_id, mi)
@read_api @read_api
def field_ids_for(self, name, book_id): def field_ids_for(self, name, book_id):

View File

@ -2,12 +2,14 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import, from __future__ import (unicode_literals, division, absolute_import,
print_function) print_function)
from future_builtins import map
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from calibre.db.tables import ONE_ONE, MANY_ONE, MANY_MANY from calibre.db.tables import ONE_ONE, MANY_ONE, MANY_MANY
from calibre.utils.icu import sort_key
class Field(object): class Field(object):
@ -16,6 +18,8 @@ class Field(object):
self.has_text_data = self.metadata['datatype'] in ('text', 'comments', self.has_text_data = self.metadata['datatype'] in ('text', 'comments',
'series', 'enumeration') 'series', 'enumeration')
self.table_type = self.table.table_type self.table_type = self.table.table_type
dt = self.metadata['datatype']
self._sort_key = (sort_key if dt == 'text' else lambda x: x)
@property @property
def metadata(self): def metadata(self):
@ -49,6 +53,18 @@ class Field(object):
''' '''
raise NotImplementedError() raise NotImplementedError()
def sort_books(self, get_metadata, all_book_ids, ascending=True):
'''
Sort books by this field. Returns a sorted list of book_ids
:param _get_metadata: A callable which when called with the book_id
returns the Metadata object for that book. Needed for sorting composite
columns.
:param all_book_ids: The set of ids for all books.
'''
raise NotImplementedError()
class OneToOneField(Field): class OneToOneField(Field):
def for_book(self, book_id, default_value=None): def for_book(self, book_id, default_value=None):
@ -66,6 +82,10 @@ class OneToOneField(Field):
def iter_book_ids(self): def iter_book_ids(self):
return self.table.book_col_map.iterkeys() return self.table.book_col_map.iterkeys()
def sort_books(self, get_metadata, all_book_ids, ascending=True):
return sorted(self.iter_book_ids(), reverse=not ascending,
key=lambda i: self._sort_key(self.book_col_map[i]))
class CompositeField(OneToOneField): class CompositeField(OneToOneField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -86,6 +106,19 @@ class CompositeField(OneToOneField):
def pop_cache(self, book_id): def pop_cache(self, book_id):
self._render_cache.pop(book_id, None) self._render_cache.pop(book_id, None)
def get_value_with_cache(self, book_id, get_metadata):
ans = self._render_cache.get(book_id, None)
if ans is None:
mi = get_metadata(book_id)
ans = mi.get(self.metadata['label'])
self._render_cache[book_id] = ans
return ans
def sort_books(self, get_metadata, all_book_ids, ascending=True):
return sorted(all_book_ids, reverse=not ascending,
key=lambda i: sort_key(self.get_value_with_cache(i,
get_metadata)))
class OnDeviceField(OneToOneField): class OnDeviceField(OneToOneField):
def __init__(self, name, table): def __init__(self, name, table):
@ -120,6 +153,10 @@ class OnDeviceField(OneToOneField):
def iter_book_ids(self): def iter_book_ids(self):
return iter(()) return iter(())
def sort_books(self, get_metadata, all_book_ids, ascending=True):
return sorted(all_book_ids, reverse=not ascending,
key=self.for_book)
class ManyToOneField(Field): class ManyToOneField(Field):
def for_book(self, book_id, default_value=None): def for_book(self, book_id, default_value=None):
@ -131,10 +168,10 @@ class ManyToOneField(Field):
return ans return ans
def ids_for_book(self, book_id): def ids_for_book(self, book_id):
ids = self.table.book_col_map.get(book_id, None) id_ = self.table.book_col_map.get(book_id, None)
if ids is None: if id_ is None:
return () return ()
return ids return (id_,)
def books_for(self, item_id): def books_for(self, item_id):
return self.table.col_book_map.get(item_id, ()) return self.table.col_book_map.get(item_id, ())
@ -142,8 +179,21 @@ class ManyToOneField(Field):
def __iter__(self): def __iter__(self):
return self.table.id_map.iterkeys() return self.table.id_map.iterkeys()
def sort_books(self, get_metadata, all_book_ids, ascending=True):
ids = sorted(self.id_map,
key=lambda i:self._sort_key(self.id_map[i]))
sm = {id_ : idx for idx, id_ in enumerate(ids)}
return sorted(all_book_ids, reverse=not ascending,
key=lambda book_id : sm.get(
self.book_col_map.get(book_id, None),
-1))
class ManyToManyField(Field): class ManyToManyField(Field):
def __init__(self, *args, **kwargs):
Field.__init__(self, *args, **kwargs)
self.alphabetical_sort = self.name != 'authors'
def for_book(self, book_id, default_value=None): def for_book(self, book_id, default_value=None):
ids = self.table.book_col_map.get(book_id, ()) ids = self.table.book_col_map.get(book_id, ())
if ids: if ids:
@ -161,6 +211,20 @@ class ManyToManyField(Field):
def __iter__(self): def __iter__(self):
return self.table.id_map.iterkeys() return self.table.id_map.iterkeys()
def sort_books(self, get_metadata, all_book_ids, ascending=True):
ids = sorted(self.id_map,
key=lambda i:self._sort_key(self.id_map[i]))
sm = {id_ : idx for idx, id_ in enumerate(ids)}
def sort_key_for_book(book_id):
item_ids = self.table.book_col_map.get(book_id, ())
if self.alphabetical_sort:
item_ids = sorted(item_ids, key=sm.get)
return tuple(map(sm.get, item_ids))
return sorted(all_book_ids, reverse=not ascending,
key=sort_key_for_book)
class AuthorsField(ManyToManyField): class AuthorsField(ManyToManyField):
def author_data(self, author_id): def author_data(self, author_id):