Merge branch 'kovidgoyal/master'

This commit is contained in:
Charles Haley 2013-07-19 14:02:11 +02:00
commit 34aa6e68b2
16 changed files with 213 additions and 102 deletions

View File

@ -70,6 +70,10 @@ class DBPrefs(dict): # {{{
self.db = db
self.defaults = {}
self.disable_setting = False
self.load_from_db()
def load_from_db(self):
self.clear()
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
try:
val = self.raw_to_object(val)
@ -136,28 +140,10 @@ class DBPrefs(dict): # {{{
@classmethod
def read_serialized(cls, library_path, recreate_prefs=False):
try:
from_filename = os.path.join(library_path,
'metadata_db_prefs_backup.json')
with open(from_filename, "rb") as f:
d = json.load(f, object_hook=from_json)
if not recreate_prefs:
return d
cls.clear()
cls.db.conn.execute('DELETE FROM preferences')
for k,v in d.iteritems():
raw = cls.to_raw(v)
cls.db.conn.execute(
'INSERT INTO preferences (key,val) VALUES (?,?)', (k, raw))
cls.db.conn.commit()
cls.clear()
cls.update(d)
return d
except:
import traceback
traceback.print_exc()
raise
return None
return json.load(f, object_hook=from_json)
# }}}
# Extra collators {{{

View File

@ -89,7 +89,6 @@ class Cache(object):
self.formatter_template_cache = {}
self.dirtied_cache = {}
self.dirtied_sequence = 0
self._search_api = Search(self, 'saved_searches', self.field_metadata.get_search_terms())
# Implement locking for all simple read/write API methods
# An unlocked version of the method is stored with the name starting
@ -105,6 +104,7 @@ class Cache(object):
lock = self.read_lock if ira else self.write_lock
setattr(self, name, wrap_simple(lock, func))
self._search_api = Search(self, 'saved_searches', self.field_metadata.get_search_terms())
self.initialize_dynamic()
@write_api
@ -127,7 +127,7 @@ class Cache(object):
except:
traceback.print_exc()
if len(self._search_api.get_saved_searches().names()):
if len(self._search_api.saved_searches.names()) > 0:
self.field_metadata.add_search_category(label='search', name=_('Searches'))
self.field_metadata.add_grouped_search_terms(
@ -140,11 +140,6 @@ class Cache(object):
if self.dirtied_cache:
self.dirtied_sequence = max(self.dirtied_cache.itervalues())+1
@property
def prefs(self):
'For internal use only (used by SavedSearchQueries). For thread-safe access to the preferences, use the pref() and set_pref() methods.'
return self.backend.prefs
@write_api
def initialize_template_cache(self):
self.formatter_template_cache = {}
@ -161,6 +156,8 @@ class Cache(object):
def reload_from_db(self, clear_caches=True):
if clear_caches:
self._clear_caches()
self.backend.prefs.load_from_db()
self._search_api.saved_searches.load_from_db()
for field in self.fields.itervalues():
if hasattr(field, 'table'):
field.table.read(self.backend) # Reread data from metadata.db
@ -1520,6 +1517,30 @@ class Cache(object):
all_paths = {self._field_for('path', book_id).partition('/')[0] for book_id in self._all_book_ids()}
self.backend.move_library_to(all_paths, newloc, progress=progress)
@read_api
def saved_search_names(self):
return self._search_api.saved_searches.names()
@read_api
def saved_search_lookup(self, name):
return self._search_api.saved_searches.lookup(name)
@write_api
def saved_search_set_all(self, smap):
self._search_api.saved_searches.set_all(smap)
@write_api
def saved_search_delete(self, name):
self._search_api.saved_searches.delete(name)
@write_api
def saved_search_add(self, name, val):
self._search_api.saved_searches.add(name, val)
@write_api
def saved_search_rename(self, old_name, new_name):
self._search_api.saved_searches.rename(old_name, new_name)
# }}}
class SortKey(object): # {{{

View File

@ -228,11 +228,10 @@ def get_categories(dbcache, sort='name', book_ids=None, icon_map=None):
icon = None
if icon_map and 'search' in icon_map:
icon = icon_map['search']
ss = dbcache._search_api.get_saved_searches()
for srch in ss.names():
items.append(Tag(srch, tooltip=ss.lookup(srch),
sort=srch, icon=icon, category='search',
is_editable=False))
queries = dbcache._search_api.saved_searches.queries
for srch in sorted(queries, key=sort_key):
items.append(Tag(srch, tooltip=queries[srch], sort=srch, icon=icon,
category='search', is_editable=False))
if len(items):
categories['search'] = items

View File

@ -120,9 +120,6 @@ class LibraryDatabase(object):
self.new_api.reload_from_db()
self.last_update_check = utcnow()
def get_saved_searches(self):
return self.new_api._search_api.get_saved_searches()
@property
def custom_column_num_map(self):
return self.backend.custom_column_num_map
@ -887,6 +884,12 @@ for meth in ('get_next_series_num_for', 'has_book', 'author_sort_from_authors'):
setattr(LibraryDatabase, meth, MT(getter(meth)))
LibraryDatabase.move_library_to = MT(lambda self, newloc, progress=None:self.new_api.move_library_to(newloc, progress=progress))
LibraryDatabase.saved_search_names = MT(lambda self:self.new_api.saved_search_names())
LibraryDatabase.saved_search_lookup = MT(lambda self, x:self.new_api.saved_search_lookup(x))
LibraryDatabase.saved_search_set_all = MT(lambda self, smap:self.new_api.saved_search_set_all(smap))
LibraryDatabase.saved_search_delete = MT(lambda self, x:self.new_api.saved_search_delete(x))
LibraryDatabase.saved_search_add = MT(lambda self, x, y:self.new_api.saved_search_add(x, y))
LibraryDatabase.saved_search_rename = MT(lambda self, x, y:self.new_api.saved_search_rename(x, y))
# Cleaning is not required anymore
LibraryDatabase.clean = LibraryDatabase.clean_custom = MT(lambda self:None)
LibraryDatabase.clean_standard_field = MT(lambda self, field, commit=False:None)

View File

@ -7,15 +7,16 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re
import re, weakref
from functools import partial
from datetime import timedelta
from calibre.constants import preferred_encoding
from calibre.utils.config_base import prefs
from calibre.utils.date import parse_date, UNDEFINED_DATE, now
from calibre.utils.icu import primary_find
from calibre.utils.icu import primary_find, sort_key
from calibre.utils.localization import lang_map, canonicalize_lang
from calibre.utils.search_query_parser import SearchQueryParser, ParseException, SavedSearchQueries
from calibre.utils.search_query_parser import SearchQueryParser, ParseException
CONTAINS_MATCH = 0
EQUALS_MATCH = 1
@ -388,11 +389,72 @@ class KeyPairSearch(object): # {{{
# }}}
class SavedSearchQueries(object): # {{{
queries = {}
opt_name = ''
def __init__(self, db, _opt_name):
self.opt_name = _opt_name
try:
self._db = weakref.ref(db)
except TypeError:
# db could be None
self._db = lambda : None
self.load_from_db()
def load_from_db(self):
db = self.db
if db is not None:
self.queries = db._pref(self.opt_name, default={})
else:
self.queries = {}
@property
def db(self):
return self._db()
def force_unicode(self, x):
if not isinstance(x, unicode):
x = x.decode(preferred_encoding, 'replace')
return x
def add(self, name, value):
db = self.db
if db is not None:
self.queries[self.force_unicode(name)] = self.force_unicode(value).strip()
db._set_pref(self.opt_name, self.queries)
def lookup(self, name):
return self.queries.get(self.force_unicode(name), None)
def delete(self, name):
db = self.db
if db is not None:
self.queries.pop(self.force_unicode(name), False)
db._set_pref(self.opt_name, self.queries)
def rename(self, old_name, new_name):
db = self.db
if db is not None:
self.queries[self.force_unicode(new_name)] = self.queries.get(self.force_unicode(old_name), None)
self.queries.pop(self.force_unicode(old_name), False)
db._set_pref(self.opt_name, self.queries)
def set_all(self, smap):
db = self.db
if db is not None:
self.queries = smap
db._set_pref(self.opt_name, smap)
def names(self):
return sorted(self.queries.iterkeys(), key=sort_key)
# }}}
class Parser(SearchQueryParser):
def __init__(self, dbcache, all_book_ids, gst, date_search, num_search,
bool_search, keypair_search, limit_search_columns, limit_search_columns_to,
locations, virtual_fields, get_saved_searches):
locations, virtual_fields, lookup_saved_search):
self.dbcache, self.all_book_ids = dbcache, all_book_ids
self.all_search_locations = frozenset(locations)
self.grouped_search_terms = gst
@ -403,7 +465,7 @@ class Parser(SearchQueryParser):
self.virtual_fields = virtual_fields or {}
if 'marked' not in self.virtual_fields:
self.virtual_fields['marked'] = self
super(Parser, self).__init__(locations, optimize=True, get_saved_searches=get_saved_searches)
super(Parser, self).__init__(locations, optimize=True, lookup_saved_search=lookup_saved_search)
@property
def field_metadata(self):
@ -693,11 +755,11 @@ class Search(object):
self.keypair_search,
prefs['limit_search_columns'],
prefs['limit_search_columns_to'], self.all_search_locations,
virtual_fields, self.get_saved_searches)
virtual_fields, self.saved_searches.lookup)
try:
ret = sqp.parse(q)
finally:
sqp.dbcache = sqp.get_saved_searches = None
sqp.dbcache = sqp.lookup_saved_search = None
return ret

View File

@ -751,3 +751,24 @@ class LegacyTest(BaseTest):
db.close()
# }}}
def test_legacy_saved_search(self): # {{{
' Test legacy saved search API '
db, ndb = self.init_old(), self.init_legacy()
run_funcs(self, db, ndb, (
('saved_search_set_all', {'one':'a', 'two':'b'}),
('saved_search_names',),
('saved_search_lookup', 'one'),
('saved_search_lookup', 'two'),
('saved_search_lookup', 'xxx'),
('saved_search_rename', 'one', '1'),
('saved_search_names',),
('saved_search_lookup', '1'),
('saved_search_delete', '1'),
('saved_search_names',),
('saved_search_add', 'n', 'm'),
('saved_search_names',),
('saved_search_lookup', 'n'),
))
# }}}

View File

@ -14,7 +14,8 @@ from calibre.gui2.dialogs.confirm_delete import confirm
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
def __init__(self, parent, initial_search=None):
from calibre.gui2.ui import saved_searches
from calibre.gui2.ui import get_gui
db = get_gui().current_db
QDialog.__init__(self, parent)
Ui_SavedSearchEditor.__init__(self)
self.setupUi(self)
@ -27,9 +28,9 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
self.current_search_name = None
self.searches = {}
for name in saved_searches().names():
self.searches[name] = saved_searches().lookup(name)
self.search_names = set([icu_lower(n) for n in saved_searches().names()])
for name in db.saved_search_names():
self.searches[name] = db.saved_search_lookup(name)
self.search_names = set([icu_lower(n) for n in db.saved_search_names()])
self.populate_search_list()
if initial_search is not None and initial_search in self.searches:
@ -98,11 +99,10 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):
self.search_text.setPlainText('')
def accept(self):
from calibre.gui2.ui import saved_searches
from calibre.gui2.ui import get_gui
db = get_gui().current_db
if self.current_search_name:
self.searches[self.current_search_name] = unicode(self.search_text.toPlainText())
for name in saved_searches().names():
saved_searches().delete(name)
for name in self.searches:
saved_searches().add(name, self.searches[name])
ss = {name:self.searches[name] for name in self.searches}
db.saved_search_set_all(ss)
QDialog.accept(self)

View File

@ -308,28 +308,35 @@ class SavedSearchBox(QComboBox): # {{{
self.saved_search_selected(self.currentText())
def saved_search_selected(self, qname):
from calibre.gui2.ui import saved_searches
from calibre.gui2.ui import get_gui
db = get_gui().current_db
qname = unicode(qname)
if qname is None or not qname.strip():
self.search_box.clear()
return
if not saved_searches().lookup(qname):
if not db.saved_search_lookup(qname):
self.search_box.clear()
self.setEditText(qname)
return
self.search_box.set_search_string(u'search:"%s"' % qname, emit_changed=False)
self.setEditText(qname)
self.setToolTip(saved_searches().lookup(qname))
self.setToolTip(db.saved_search_lookup(qname))
def initialize_saved_search_names(self):
from calibre.gui2.ui import saved_searches
qnames = saved_searches().names()
self.addItems(qnames)
from calibre.gui2.ui import get_gui
gui = get_gui()
try:
names = gui.current_db.saved_search_names()
except AttributeError:
# Happens during gui initialization
names = []
self.addItems(names)
self.setCurrentIndex(-1)
# SIGNALed from the main UI
def save_search_button_clicked(self):
from calibre.gui2.ui import saved_searches
from calibre.gui2.ui import get_gui
db = get_gui().current_db
name = unicode(self.currentText())
if not name.strip():
name = unicode(self.search_box.text()).replace('"', '')
@ -337,8 +344,8 @@ class SavedSearchBox(QComboBox): # {{{
error_dialog(self, _('Create saved search'),
_('There is no search to save'), show=True)
return
saved_searches().delete(name)
saved_searches().add(name, unicode(self.search_box.text()))
db.saved_search_delete(name)
db.saved_search_add(name, unicode(self.search_box.text()))
# now go through an initialization cycle to ensure that the combobox has
# the new search in it, that it is selected, and that the search box
# references the new search instead of the text in the search.
@ -348,7 +355,8 @@ class SavedSearchBox(QComboBox): # {{{
self.changed.emit()
def delete_current_search(self):
from calibre.gui2.ui import saved_searches
from calibre.gui2.ui import get_gui
db = get_gui().current_db
idx = self.currentIndex()
if idx <= 0:
error_dialog(self, _('Delete current search'),
@ -358,21 +366,22 @@ class SavedSearchBox(QComboBox): # {{{
'<b>permanently deleted</b>. Are you sure?')
+'</p>', 'saved_search_delete', self):
return
ss = saved_searches().lookup(unicode(self.currentText()))
ss = db.saved_search_lookup(unicode(self.currentText()))
if ss is None:
return
saved_searches().delete(unicode(self.currentText()))
db.saved_search_delete(unicode(self.currentText()))
self.clear()
self.search_box.clear()
self.changed.emit()
# SIGNALed from the main UI
def copy_search_button_clicked(self):
from calibre.gui2.ui import saved_searches
from calibre.gui2.ui import get_gui
db = get_gui().current_db
idx = self.currentIndex()
if idx < 0:
return
self.search_box.set_search_string(saved_searches().lookup(unicode(self.currentText())))
self.search_box.set_search_string(db.saved_search_lookup(unicode(self.currentText())))
# }}}

View File

@ -178,7 +178,7 @@ class CreateVirtualLibrary(QDialog): # {{{
self.resize(self.sizeHint()+QSize(150, 25))
def search_text_changed(self, txt):
from calibre.gui2.ui import saved_searches
db = self.gui.current_db
searches = [_('Saved searches recognized in the expression:')]
txt = unicode(txt)
while txt:
@ -201,9 +201,9 @@ class CreateVirtualLibrary(QDialog): # {{{
search_name = possible_search[0]
if search_name.startswith('='):
search_name = search_name[1:]
if search_name in saved_searches().names():
if search_name in db.saved_search_names():
searches.append(search_name + '=' +
saved_searches().lookup(search_name))
db.saved_search_lookup(search_name))
else:
txt = ''
else:
@ -234,18 +234,17 @@ class CreateVirtualLibrary(QDialog): # {{{
self.vl_text.setText(self.original_search)
def link_activated(self, url):
from calibre.gui2.ui import saved_searches
db = self.gui.current_db
f, txt = unicode(url).partition('.')[0::2]
if f == 'search':
names = saved_searches().names()
names = db.saved_search_names()
else:
names = getattr(db, 'all_%s_names'%f)()
d = SelectNames(names, txt, parent=self)
if d.exec_() == d.Accepted:
prefix = f+'s' if f in {'tag', 'author'} else f
if f == 'search':
search = ['(%s)'%(saved_searches().lookup(x)) for x in d.names]
search = ['(%s)'%(db.saved_search_lookup(x)) for x in d.names]
else:
search = ['%s:"=%s"'%(prefix, x.replace('"', '\\"')) for x in d.names]
if search:
@ -476,7 +475,7 @@ class SearchRestrictionMixin(object):
return name[0:MAX_VIRTUAL_LIBRARY_NAME_LENGTH].strip()
def build_search_restriction_list(self):
from calibre.gui2.ui import saved_searches
from calibre.gui2.ui import get_gui
m = self.ar_menu
m.clear()
@ -508,7 +507,7 @@ class SearchRestrictionMixin(object):
add_action(current_restriction_text, 2)
dex += 1
for n in sorted(saved_searches().names(), key=sort_key):
for n in sorted(get_gui().current_db.saved_search_names(), key=sort_key):
add_action(n, dex)
dex += 1

View File

@ -878,7 +878,7 @@ class TagsModel(QAbstractItemModel): # {{{
traceback.print_exc()
self.db.data.change_search_locations(self.db.field_metadata.get_search_terms())
if len(self.db.get_saved_searches().names()):
if len(self.db.saved_search_names()):
tb_cats.add_search_category(label='search', name=_('Searches'))
if self.filter_categories_by:
@ -1004,11 +1004,11 @@ class TagsModel(QAbstractItemModel): # {{{
_('Author names cannot contain & characters.')).exec_()
return False
if key == 'search':
if val in self.db.get_saved_searches().names():
if val in self.db.saved_search_names():
error_dialog(self.gui_parent, _('Duplicate search name'),
_('The saved search name %s is already used.')%val).exec_()
return False
self.db.get_saved_searches().rename(unicode(item.data(role).toString()), val)
self.db.saved_search_rename(unicode(item.data(role).toString()), val)
item.tag.name = val
self.search_item_renamed.emit() # Does a refresh
else:

View File

@ -354,8 +354,7 @@ class TagsView(QTreeView): # {{{
self.delete_user_category.emit(key)
return
if action == 'delete_search':
from calibre.gui2.ui import saved_searches
saved_searches().delete(key)
self.model().db.saved_search_delete(key)
self.rebuild_saved_searches.emit()
return
if action == 'delete_item_from_user_category':

View File

@ -98,16 +98,6 @@ _gui = None
def get_gui():
return _gui
def saved_searches():
'Return the saved searches defined in the currently open library'
try:
return _gui.library_view.model().db.get_saved_searches()
except AttributeError:
# Happens during initialization of the gui
from calibre.utils.search_query_parser import saved_searches
return saved_searches()
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
@ -312,10 +302,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
####################### Search boxes ########################
SearchRestrictionMixin.__init__(self)
SavedSearchBoxMixin.__init__(self)
SearchBoxMixin.__init__(self)
####################### Library view ########################
LibraryViewMixin.__init__(self, db)
SearchBoxMixin.__init__(self) # Requires current_db
if show_gui:
self.show()

View File

@ -1029,11 +1029,10 @@ def command_saved_searches(args, dbpath):
prints(_('Error: You must specify an action (add|remove|list)'), file=sys.stderr)
return 1
db = get_db(dbpath, opts)
ss = db.get_saved_searches()
if args[0] == 'list':
for name in ss.names():
for name in db.saved_search_names():
prints(_('Name:'), name)
prints(_('Search string:'), ss.lookup(name))
prints(_('Search string:'), db.saved_search_lookup(name))
print
elif args[0] == 'add':
if len(args) < 3:
@ -1041,7 +1040,7 @@ def command_saved_searches(args, dbpath):
print
prints(_('Error: You must specify a name and a search string'), file=sys.stderr)
return 1
ss.add(args[1], args[2])
db.saved_search_add(args[1], args[2])
prints(args[1], _('added'))
elif args[0] == 'remove':
if len(args) < 2:
@ -1049,7 +1048,7 @@ def command_saved_searches(args, dbpath):
print
prints(_('Error: You must specify a name'), file=sys.stderr)
return 1
ss.delete(args[1])
db.saved_search_delete(args[1])
prints(args[1], _('removed'))
else:
parser.print_help()

View File

@ -538,8 +538,23 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if self.user_version == 0:
self.user_version = 1
def get_saved_searches(self):
return saved_searches()
def saved_search_names(self):
return saved_searches().names()
def saved_search_rename(self, old_name, new_name):
saved_searches().rename(old_name, new_name)
def saved_search_lookup(self, name):
return saved_searches().lookup(name)
def saved_search_add(self, name, val):
saved_searches().add(name, val)
def saved_search_delete(self, name):
saved_searches().delete(name)
def saved_search_set_all(self, smap):
saved_searches().set_all(smap)
def last_modified(self):
''' Return last modified time as a UTC datetime object'''

View File

@ -209,7 +209,7 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
if sr:
if sr in virt_libs:
sr = virt_libs[sr]
elif sr not in self.db.get_saved_searches().names():
elif sr not in self.db.saved_search_names():
prints('WARNING: Content server: search restriction ',
sr, ' does not exist')
sr = ''

View File

@ -76,6 +76,11 @@ class SavedSearchQueries(object):
self.queries.pop(self.force_unicode(old_name), False)
db.prefs[self.opt_name] = self.queries
def set_all(self, smap):
db = self.db
if db is not None:
self.queries = db.prefs[self.opt_name] = smap
def names(self):
return sorted(self.queries.keys(),key=sort_key)
@ -93,6 +98,9 @@ def saved_searches():
global ss
return ss
def global_lookup_saved_search(name):
return ss.lookup(name)
'''
Parse a search expression into a series of potentially recursive operations.
@ -292,10 +300,10 @@ class SearchQueryParser(object):
failed.append(test[0])
return failed
def __init__(self, locations, test=False, optimize=False, get_saved_searches=None):
def __init__(self, locations, test=False, optimize=False, lookup_saved_search=None):
self.sqp_initialize(locations, test=test, optimize=optimize)
self.parser = Parser()
self.get_saved_searches = saved_searches if get_saved_searches is None else get_saved_searches
self.lookup_saved_search = global_lookup_saved_search if lookup_saved_search is None else lookup_saved_search
def sqp_change_locations(self, locations):
self.sqp_initialize(locations, optimize=self.optimize)
@ -368,7 +376,7 @@ class SearchQueryParser(object):
raise ParseException(_('Recursive saved search: {0}').format(query))
if self.recurse_level > 5:
self.searches_seen.add(query)
return self._parse(self.get_saved_searches().lookup(query), candidates)
return self._parse(self.lookup_saved_search(query), candidates)
except ParseException as e:
raise e
except: # convert all exceptions (e.g., missing key) to a parse error