Store: MobileRead: Boolean and field search. Advanced search builder.

This commit is contained in:
John Schember 2011-04-23 15:43:32 -04:00
parent 568b79c018
commit 687b799889
3 changed files with 147 additions and 70 deletions

View File

@ -10,12 +10,13 @@ import difflib
import heapq import heapq
import time import time
from contextlib import closing from contextlib import closing
from operator import attrgetter
from threading import RLock from threading import RLock
from lxml import html from lxml import html
from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVariant, \ from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVariant, \
pyqtSignal pyqtSignal, QIcon
from calibre import browser from calibre import browser
from calibre.gui2 import open_url, NONE from calibre.gui2 import open_url, NONE
@ -24,7 +25,11 @@ from calibre.gui2.store.basic_config import BasicStoreConfig
from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog
from calibre.gui2.store.search_result import SearchResult from calibre.gui2.store.search_result import SearchResult
from calibre.gui2.store.web_store_dialog import WebStoreDialog from calibre.gui2.store.web_store_dialog import WebStoreDialog
from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.search_query_parser import SearchQueryParser
class MobileReadStore(BasicStoreConfig, StorePlugin): class MobileReadStore(BasicStoreConfig, StorePlugin):
@ -50,28 +55,10 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
def search(self, query, max_results=10, timeout=60): def search(self, query, max_results=10, timeout=60):
books = self.get_book_list(timeout=timeout) books = self.get_book_list(timeout=timeout)
query = query.lower() sf = SearchFilter(books)
query_parts = query.split(' ') matches = sf.parse(query)
matches = []
s = difflib.SequenceMatcher() for book in matches:
for x in books:
ratio = 0
t_string = '%s %s' % (x.author.lower(), x.title.lower())
query_matches = []
for q in query_parts:
if q in t_string:
query_matches.append(q)
for q in query_matches:
s.set_seq2(q)
for p in t_string.split(' '):
s.set_seq1(p)
ratio += s.ratio()
if ratio > 0:
matches.append((ratio, x))
# Move the best scorers to head of list.
matches = heapq.nlargest(max_results, matches)
for score, book in matches:
book.price = '$0.00' book.price = '$0.00'
book.drm = SearchResult.DRM_UNLOCKED book.drm = SearchResult.DRM_UNLOCKED
yield book yield book
@ -153,23 +140,38 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog):
self.plugin = plugin self.plugin = plugin
self.model = BooksModel() self.adv_search_button.setIcon(QIcon(I('search.png')))
self.results_view.setModel(self.model)
self.results_view.model().set_books(self.plugin.get_book_list()) self._model = BooksModel(self.plugin.get_book_list())
self.total.setText('%s' % self.model.rowCount()) self.results_view.setModel(self._model)
self.total.setText('%s' % self.results_view.model().rowCount())
self.search_button.clicked.connect(self.do_search)
self.adv_search_button.clicked.connect(self.build_adv_search)
self.results_view.activated.connect(self.open_store) self.results_view.activated.connect(self.open_store)
self.search_query.textChanged.connect(self.model.set_filter) self.results_view.model().total_changed.connect(self.update_book_total)
self.results_view.model().total_changed.connect(self.total.setText)
self.finished.connect(self.dialog_closed) self.finished.connect(self.dialog_closed)
self.restore_state() self.restore_state()
def do_search(self):
self.results_view.model().search(unicode(self.search_query.text()))
def open_store(self, index): def open_store(self, index):
result = self.results_view.model().get_book(index) result = self.results_view.model().get_book(index)
if result: if result:
self.plugin.open(self, result.detail_item) self.plugin.open(self, result.detail_item)
def update_book_total(self, total):
self.total.setText('%s' % total)
def build_adv_search(self):
adv = AdvSearchBuilderDialog(self)
adv.price_label.hide()
adv.price_box.hide()
if adv.exec_() == QDialog.Accepted:
self.search_query.setText(adv.search_string())
def restore_state(self): def restore_state(self):
geometry = self.plugin.config.get('dialog_geometry', None) geometry = self.plugin.config.get('dialog_geometry', None)
if geometry: if geometry:
@ -192,7 +194,7 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog):
def save_state(self): def save_state(self):
self.plugin.config['dialog_geometry'] = bytearray(self.saveGeometry()) self.plugin.config['dialog_geometry'] = bytearray(self.saveGeometry())
self.plugin.config['dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())] self.plugin.config['dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())]
self.plugin.config['dialog_sort_col'] = self.results_view.model().sort_col self.plugin.config['dialog_sort_col'] = self.results_view.model().sort_col
self.plugin.config['dialog_sort_order'] = self.results_view.model().sort_order self.plugin.config['dialog_sort_order'] = self.results_view.model().sort_order
@ -202,24 +204,19 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog):
class BooksModel(QAbstractItemModel): class BooksModel(QAbstractItemModel):
total_changed = pyqtSignal(unicode) total_changed = pyqtSignal(int)
HEADERS = [_('Title'), _('Author(s)'), _('Format')] HEADERS = [_('Title'), _('Author(s)'), _('Format')]
def __init__(self): def __init__(self, all_books):
QAbstractItemModel.__init__(self) QAbstractItemModel.__init__(self)
self.books = [] self.books = all_books
self.all_books = [] self.all_books = all_books
self.filter = '' self.filter = ''
self.search_filter = SearchFilter(all_books)
self.sort_col = 0 self.sort_col = 0
self.sort_order = Qt.AscendingOrder self.sort_order = Qt.AscendingOrder
def set_books(self, books):
self.books = books
self.all_books = books
self.sort(self.sort_col, self.sort_order)
def get_book(self, index): def get_book(self, index):
row = index.row() row = index.row()
if row < len(self.books): if row < len(self.books):
@ -227,32 +224,17 @@ class BooksModel(QAbstractItemModel):
else: else:
return None return None
def set_filter(self, filter): def search(self, filter):
#self.layoutAboutToBeChanged.emit() self.filter = filter.strip()
self.beginResetModel() if not self.filter:
self.filter = unicode(filter)
self.books = []
if self.filter:
for b in self.all_books:
test = '%s %s %s' % (b.title, b.author, b.formats)
test = test.lower()
include = True
for item in self.filter.split(' '):
item = item.lower()
if item not in test:
include = False
break
if include:
self.books.append(b)
else:
self.books = self.all_books self.books = self.all_books
else:
self.sort(self.sort_col, self.sort_order, reset=False) try:
self.total_changed.emit('%s' % self.rowCount()) self.books = list(self.search_filter.parse(self.filter))
except:
self.endResetModel() self.books = self.all_books
#self.layoutChanged.emit() self.sort(self.sort_col, self.sort_order)
self.total_changed.emit(self.rowCount())
def index(self, row, column, parent=QModelIndex()): def index(self, row, column, parent=QModelIndex()):
return self.createIndex(row, column) return self.createIndex(row, column)
@ -304,13 +286,91 @@ class BooksModel(QAbstractItemModel):
def sort(self, col, order, reset=True): def sort(self, col, order, reset=True):
self.sort_col = col self.sort_col = col
self.sort_order = order self.sort_order = order
if not self.books: if not self.books:
return return
descending = order == Qt.DescendingOrder descending = order == Qt.DescendingOrder
self.books.sort(None, self.books.sort(None,
lambda x: sort_key(unicode(self.data_as_text(x, col))), lambda x: sort_key(unicode(self.data_as_text(x, col))),
descending) descending)
if reset: if reset:
self.reset() self.reset()
class SearchFilter(SearchQueryParser):
USABLE_LOCATIONS = [
'all',
'author',
'authors',
'format',
'formats',
'title',
]
def __init__(self, all_books=[]):
SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
self.srs = set(all_books)
def universal_set(self):
return self.srs
def get_matches(self, location, query):
location = location.lower().strip()
if location == 'authors':
location = 'author'
elif location == 'formats':
location = 'format'
matchkind = CONTAINS_MATCH
if len(query) > 1:
if query.startswith('\\'):
query = query[1:]
elif query.startswith('='):
matchkind = EQUALS_MATCH
query = query[1:]
elif query.startswith('~'):
matchkind = REGEXP_MATCH
query = query[1:]
if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D
query = query.lower()
if location not in self.USABLE_LOCATIONS:
return set([])
matches = set([])
all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
locations = all_locs if location == 'all' else [location]
q = {
'author': lambda x: x.author.lower(),
'format': attrgetter('formats'),
'title': lambda x: x.title.lower(),
}
for x in ('author', 'format'):
q[x+'s'] = q[x]
for sr in self.srs:
for locvalue in locations:
accessor = q[locvalue]
if query == 'true':
if accessor(sr) is not None:
matches.add(sr)
continue
if query == 'false':
if accessor(sr) is None:
matches.add(sr)
continue
try:
### Can't separate authors because comma is used for name sep and author sep
### Exact match might not get what you want. For that reason, turn author
### exactmatch searches into contains searches.
if locvalue == 'author' and matchkind == EQUALS_MATCH:
m = CONTAINS_MATCH
else:
m = matchkind
vals = [accessor(sr)]
if _match(query, vals, m):
matches.add(sr)
break
except ValueError: # Unicode errors
import traceback
traceback.print_exc()
return matches

View File

@ -19,13 +19,30 @@
<item> <item>
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Search:</string> <string>&amp;Query:</string>
</property>
<property name="buddy">
<cstring>search_query</cstring>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="adv_search_button">
<property name="text">
<string>...</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLineEdit" name="search_query"/> <widget class="QLineEdit" name="search_query"/>
</item> </item>
<item>
<widget class="QPushButton" name="search_button">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
<item> <item>

View File

@ -50,7 +50,7 @@
<item row="2" column="0" colspan="2"> <item row="2" column="0" colspan="2">
<widget class="QTabWidget" name="tabWidget"> <widget class="QTabWidget" name="tabWidget">
<property name="currentIndex"> <property name="currentIndex">
<number>1</number> <number>0</number>
</property> </property>
<widget class="QWidget" name="tab"> <widget class="QWidget" name="tab">
<attribute name="title"> <attribute name="title">
@ -217,7 +217,7 @@
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_9"> <widget class="QLabel" name="price_label">
<property name="text"> <property name="text">
<string>&amp;Price:</string> <string>&amp;Price:</string>
</property> </property>