From 2cb241b205224202f01b452ed7310fca302c62cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?=
Date: Mon, 23 May 2011 22:56:39 +0200
Subject: [PATCH 01/30] legimi store
---
src/calibre/customize/builtins.py | 11 ++++
src/calibre/gui2/store/legimi_plugin.py | 75 +++++++++++++++++++++++++
2 files changed, 86 insertions(+)
create mode 100644 src/calibre/gui2/store/legimi_plugin.py
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 680e36d0f3..d9e8be00b5 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -1272,6 +1272,16 @@ class StoreKoboStore(StoreBase):
headquarters = 'CA'
formats = ['EPUB']
+class StoreLegimiStore(StoreBase):
+ name = 'Legimi'
+ author = u'Tomasz Długosz'
+ description = u'Tanie oraz darmowe ebooki, egazety i blogi w formacie EPUB, wprost na Twój e-czytnik, iPhone, iPad, Android i komputer'
+ actual_plugin = 'calibre.gui2.store.legimi_plugin:LegimiStore'
+
+ drm_free_only = False
+ headquarters = 'PL'
+ formats = ['EPUB']
+
class StoreManyBooksStore(StoreBase):
name = 'ManyBooks'
description = u'Public domain and creative commons works from many sources.'
@@ -1393,6 +1403,7 @@ plugins += [
StoreGoogleBooksStore,
StoreGutenbergStore,
StoreKoboStore,
+ StoreLegimiStore,
StoreManyBooksStore,
StoreMobileReadStore,
StoreNextoStore,
diff --git a/src/calibre/gui2/store/legimi_plugin.py b/src/calibre/gui2/store/legimi_plugin.py
new file mode 100644
index 0000000000..7212f0f394
--- /dev/null
+++ b/src/calibre/gui2/store/legimi_plugin.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, Tomasz Długosz '
+__docformat__ = 'restructuredtext en'
+
+import re
+import urllib
+from contextlib import closing
+
+from lxml import html
+
+from PyQt4.Qt import QUrl
+
+from calibre import browser, url_slash_cleaner
+from calibre.gui2 import open_url
+from calibre.gui2.store import StorePlugin
+from calibre.gui2.store.basic_config import BasicStoreConfig
+from calibre.gui2.store.search_result import SearchResult
+from calibre.gui2.store.web_store_dialog import WebStoreDialog
+
+class LegimiStore(BasicStoreConfig, StorePlugin):
+
+ def open(self, parent=None, detail_item=None, external=False):
+
+ url = 'http://www.legimi.com/pl/ebooks/?price=any'
+ detail_url = None
+
+ if detail_item:
+ detail_url = detail_item
+
+ if external or self.config.get('open_external', False):
+ open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
+ else:
+ d = WebStoreDialog(self.gui, url, parent, detail_url)
+ d.setWindowTitle(self.name)
+ d.set_tags(self.config.get('tags', ''))
+ d.exec_()
+
+ def search(self, query, max_results=10, timeout=60):
+ url = 'http://www.legimi.com/pl/ebooks/?price=any&lang=pl&search=' + urllib.quote_plus(query.encode('utf-8')) + '&sort=relevance'
+
+ br = browser()
+
+ counter = max_results
+ with closing(br.open(url, timeout=timeout)) as f:
+ doc = html.fromstring(f.read())
+ for data in doc.xpath('//div[@class="list"]/ul/li'):
+ if counter <= 0:
+ break
+
+ id = ''.join(data.xpath('.//div[@class="item_cover_container"]/a[1]/@href'))
+ if not id:
+ continue
+
+ cover_url = ''.join(data.xpath('.//div[@class="item_cover_container"]/a/img/@src'))
+ title = ''.join(data.xpath('.//div[@class="item_entries"]/h2/a/text()'))
+ author = ''.join(data.xpath('.//div[@class="item_entries"]/span[1]/a/text()'))
+ price = ''.join(data.xpath('.//div[@class="item_entries"]/span[3]/text()'))
+ price = re.sub(r'[^0-9,]*','',price) + ' zł'
+
+ counter -= 1
+
+ s = SearchResult()
+ s.cover_url = 'http://www.legimi.com/' + cover_url
+ s.title = title.strip()
+ s.author = author.strip()
+ s.price = price
+ s.detail_item = 'http://www.legimi.com/' + id.strip()
+ s.drm = SearchResult.DRM_LOCKED
+ s.formats = 'EPUB'
+
+ yield s
From 99ad2c86b5cd71541b2f9370700ee14ec908c7f3 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 24 May 2011 08:23:50 +0100
Subject: [PATCH 02/30] Small change to help text. Add tooltip to wizard
button. Have tag column expand in tag wizard when resizing dialog.
---
src/calibre/gui2/dialogs/template_line_editor.py | 1 +
src/calibre/gui2/preferences/look_feel.py | 4 ++--
src/calibre/gui2/preferences/look_feel.ui | 15 +++++++++++++++
3 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py
index 95727e442b..26f4dd0753 100644
--- a/src/calibre/gui2/dialogs/template_line_editor.py
+++ b/src/calibre/gui2/dialogs/template_line_editor.py
@@ -63,6 +63,7 @@ class TagWizard(QDialog):
self.tags = tags
l = QGridLayout()
self.setLayout(l)
+ l.setColumnStretch(0, 1)
l.addWidget(QLabel(_('Tag Value')), 0, 0, 1, 1)
l.addWidget(QLabel(_('Color')), 0, 1, 1, 1)
self.tagboxes = []
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index 483934825d..49bfb1df1a 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -167,8 +167,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
''
'tutorial on using templates.') +
'
' +
- _('If you want to color a field based on tags, then right-click '
- 'in an empty template line and choose tags wizard. '
+ _('If you want to color a field based on tags, then click the '
+ 'button next to an empty line to open the tags wizard. '
'It will build a template for you. You can later edit that '
'template with the same wizard. If you edit it by hand, the '
'wizard might not work or might restore old values.') +
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index a67a3585cb..fe6134f235 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -442,6 +442,9 @@ then the tags will be displayed each on their own line.
:/images/wizard.png:/images/wizard.png
+
+ Open the tags wizard.
+
-
@@ -456,6 +459,9 @@ then the tags will be displayed each on their own line.
:/images/wizard.png:/images/wizard.png
+
+ Open the tags wizard.
+
-
@@ -470,6 +476,9 @@ then the tags will be displayed each on their own line.
:/images/wizard.png:/images/wizard.png
+
+ Open the tags wizard.
+
-
@@ -484,6 +493,9 @@ then the tags will be displayed each on their own line.
:/images/wizard.png:/images/wizard.png
+
+ Open the tags wizard.
+
-
@@ -498,6 +510,9 @@ then the tags will be displayed each on their own line.
:/images/wizard.png:/images/wizard.png
+
+ Open the tags wizard.
+
-
From 1bb4911ece2374f3fc2fce0e57383fc11f847e8c Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 24 May 2011 10:54:06 -0600
Subject: [PATCH 03/30] ...
---
src/calibre/linux.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/calibre/linux.py b/src/calibre/linux.py
index 1e7a62b869..9e58d4f638 100644
--- a/src/calibre/linux.py
+++ b/src/calibre/linux.py
@@ -356,7 +356,7 @@ class PostInstall:
mimetypes = set([])
for x in all_input_formats():
mt = guess_type('dummy.'+x)[0]
- if mt and 'chemical' not in mt:
+ if mt and 'chemical' not in mt and 'ctc-posml' not in mt:
mimetypes.add(mt)
def write_mimetypes(f):
@@ -376,11 +376,10 @@ class PostInstall:
des = ('calibre-gui.desktop', 'calibre-lrfviewer.desktop',
'calibre-ebook-viewer.desktop')
for x in des:
- cmd = ['xdg-desktop-menu', 'install', './'+x]
- if x != des[-1]:
- cmd.insert(2, '--noupdate')
+ cmd = ['xdg-desktop-menu', 'install', '--noupdate', './'+x]
check_call(' '.join(cmd), shell=True)
self.menu_resources.append(x)
+ check_call(['xdg-desktop-menu', 'forceupdate'])
f = open('calibre-mimetypes', 'wb')
f.write(MIME)
f.close()
From 268a81499cde8ae38063679343306113148b158a Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 24 May 2011 18:26:04 +0100
Subject: [PATCH 04/30] Change control to a line edit. Improve generated
template code.
---
.../gui2/dialogs/template_line_editor.py | 17 +++++++++--------
1 file changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py
index 26f4dd0753..98b74b391d 100644
--- a/src/calibre/gui2/dialogs/template_line_editor.py
+++ b/src/calibre/gui2/dialogs/template_line_editor.py
@@ -9,7 +9,7 @@ from PyQt4.Qt import (QLineEdit, QDialog, QGridLayout, QLabel,
QDialogButtonBox, QColor, QComboBox, QIcon)
from calibre.gui2.dialogs.template_dialog import TemplateDialog
-from calibre.gui2.complete import MultiCompleteComboBox
+from calibre.gui2.complete import MultiCompleteLineEdit
from calibre.gui2 import error_dialog
class TemplateLineEditor(QLineEdit):
@@ -64,14 +64,15 @@ class TagWizard(QDialog):
l = QGridLayout()
self.setLayout(l)
l.setColumnStretch(0, 1)
- l.addWidget(QLabel(_('Tag Value')), 0, 0, 1, 1)
+ l.setColumnMinimumWidth(0, 300)
+ l.addWidget(QLabel(_('Tags (more than one per box permitted)')), 0, 0, 1, 1)
l.addWidget(QLabel(_('Color')), 0, 1, 1, 1)
self.tagboxes = []
self.colorboxes = []
self.colors = [unicode(s) for s in list(QColor.colorNames())]
self.colors.insert(0, '')
for i in range(0, 10):
- tb = MultiCompleteComboBox(self)
+ tb = MultiCompleteLineEdit(self)
tb.set_separator(', ')
tb.update_items_cache(self.tags)
self.tagboxes.append(tb)
@@ -102,10 +103,11 @@ class TagWizard(QDialog):
def accepted(self):
res = ("program:\n#tag wizard -- do not directly edit\n"
- " t = field('tags');\n first_non_empty(\n")
+ " t = field('tags');\n first_non_empty(\n")
lines = []
for tb, cb in zip(self.tagboxes, self.colorboxes):
- tags = [t.strip() for t in unicode(tb.currentText()).split(',') if t.strip()]
+ tags = [t.strip() for t in unicode(tb.text()).split(',') if t.strip()]
+ tags = '$|^'.join(tags)
c = unicode(cb.currentText()).strip()
if not tags or not c:
continue
@@ -114,14 +116,13 @@ class TagWizard(QDialog):
_('The color {0} is not valid').format(c),
show=True, show_copy_button=False)
return False
- for t in tags:
- lines.append(" in_list(t, ',', '^{0}$', '{1}', '')".format(t, c))
+ lines.append(" in_list(t, ',', '^{0}$', '{1}', '')".format(tags, c))
res += ',\n'.join(lines)
res += ')\n'
self.template = res
res = ''
for tb, cb in zip(self.tagboxes, self.colorboxes):
- t = unicode(tb.currentText()).strip()
+ t = unicode(tb.text()).strip()
if t.endswith(','):
t = t[:-1]
c = unicode(cb.currentText()).strip()
From 2fec4aa6c3719767d52c8e4558697dbc4f088ebb Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 24 May 2011 13:33:52 -0600
Subject: [PATCH 05/30] ...
---
src/calibre/library/server/base.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py
index 862e724809..319feefa44 100644
--- a/src/calibre/library/server/base.py
+++ b/src/calibre/library/server/base.py
@@ -221,7 +221,12 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
if not ip or ip.startswith('127.'):
raise
cherrypy.log('Trying to bind to single interface: '+ip)
+ # Change the host we listen on
cherrypy.config.update({'server.socket_host' : ip})
+ # This ensures that the change is actually applied
+ cherrypy.server.socket_host = ip
+ cherrypy.server.httpserver = cherrypy.server.instance = None
+
cherrypy.engine.start()
self.is_running = True
@@ -231,6 +236,8 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
cherrypy.engine.block()
except Exception as e:
self.exception = e
+ import traceback
+ traceback.print_exc()
finally:
self.is_running = False
try:
From 3a78a875afe7c980aae8497d89e1a1d784335c2f Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 24 May 2011 15:18:47 -0600
Subject: [PATCH 06/30] Amazon metadata download: Use separate identifiers for
country specific downloads so that the links to Amazon in the Book details
panel work when downloading metadata from country specific amazon websites.
Fixes #786146 (German Amazon Metadata)
---
src/calibre/ebooks/metadata/sources/amazon.py | 63 +++++++++++++------
1 file changed, 43 insertions(+), 20 deletions(-)
diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py
index f291959475..7da37ce5af 100644
--- a/src/calibre/ebooks/metadata/sources/amazon.py
+++ b/src/calibre/ebooks/metadata/sources/amazon.py
@@ -29,7 +29,7 @@ class Worker(Thread): # Get details {{{
Get book details from amazons book page in a separate thread
'''
- def __init__(self, url, result_queue, browser, log, relevance, plugin, timeout=20):
+ def __init__(self, url, result_queue, browser, log, relevance, domain, plugin, timeout=20):
Thread.__init__(self)
self.daemon = True
self.url, self.result_queue = url, result_queue
@@ -37,7 +37,7 @@ class Worker(Thread): # Get details {{{
self.relevance, self.plugin = relevance, plugin
self.browser = browser.clone_browser()
self.cover_url = self.amazon_id = self.isbn = None
- self.domain = self.plugin.domain
+ self.domain = domain
months = {
'de': {
@@ -199,7 +199,8 @@ class Worker(Thread): # Get details {{{
return
mi = Metadata(title, authors)
- mi.set_identifier('amazon', asin)
+ idtype = 'amazon' if self.domain == 'com' else 'amazon_'+self.domain
+ mi.set_identifier(idtype, asin)
self.amazon_id = asin
try:
@@ -404,12 +405,30 @@ class Amazon(Source):
'country\'s Amazon website.'), choices=AMAZON_DOMAINS),
)
+ def get_domain_and_asin(self, identifiers):
+ for key, val in identifiers.iteritems():
+ key = key.lower()
+ if key in ('amazon', 'asin'):
+ return 'com', val
+ if key.startswith('amazon_'):
+ domain = key.split('_')[-1]
+ if domain and domain in self.AMAZON_DOMAINS:
+ return domain, val
+ return None, None
+
def get_book_url(self, identifiers): # {{{
- asin = identifiers.get('amazon', None)
- if asin is None:
- asin = identifiers.get('asin', None)
- if asin:
- return ('amazon', asin, 'http://amzn.com/%s'%asin)
+ domain, asin = self.get_domain_and_asin(identifiers)
+ if domain and asin:
+ url = None
+ if domain == 'com':
+ url = 'http://amzn.com/'+asin
+ elif domain == 'uk':
+ url = 'http://www.amazon.co.uk/dp/'+asin
+ else:
+ url = 'http://www.amazon.%s/dp/%s'%(domain, asin)
+ if url:
+ idtype = 'amazon' if self.domain == 'com' else 'amazon_'+self.domain
+ return (idtype, asin, url)
# }}}
@property
@@ -420,8 +439,14 @@ class Amazon(Source):
return domain
- def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
- domain = self.domain
+ def create_query(self, log, title=None, authors=None, identifiers={}, # {{{
+ domain=None):
+ if domain is None:
+ domain = self.domain
+
+ idomain, asin = self.get_domain_and_asin(identifiers)
+ if idomain is not None:
+ domain = idomain
# See the amazon detailed search page to get all options
q = { 'search-alias' : 'aps',
@@ -433,7 +458,6 @@ class Amazon(Source):
else:
q['sort'] = 'relevancerank'
- asin = identifiers.get('amazon', None)
isbn = check_isbn(identifiers.get('isbn', None))
if asin is not None:
@@ -456,23 +480,22 @@ class Amazon(Source):
if not ('field-keywords' in q or 'field-isbn' in q or
('field-title' in q)):
# Insufficient metadata to make an identify query
- return None
+ return None, None
latin1q = dict([(x.encode('latin1', 'ignore'), y.encode('latin1',
'ignore')) for x, y in
q.iteritems()])
+ udomain = domain
if domain == 'uk':
- domain = 'co.uk'
- url = 'http://www.amazon.%s/s/?'%domain + urlencode(latin1q)
- return url
+ udomain = 'co.uk'
+ url = 'http://www.amazon.%s/s/?'%udomain + urlencode(latin1q)
+ return url, domain
# }}}
def get_cached_cover_url(self, identifiers): # {{{
url = None
- asin = identifiers.get('amazon', None)
- if asin is None:
- asin = identifiers.get('asin', None)
+ domain, asin = self.get_domain_and_asin(identifiers)
if asin is None:
isbn = identifiers.get('isbn', None)
if isbn is not None:
@@ -489,7 +512,7 @@ class Amazon(Source):
Note this method will retry without identifiers automatically if no
match is found with identifiers.
'''
- query = self.create_query(log, title=title, authors=authors,
+ query, domain = self.create_query(log, title=title, authors=authors,
identifiers=identifiers)
if query is None:
log.error('Insufficient metadata to construct query')
@@ -571,7 +594,7 @@ class Amazon(Source):
log.error('No matches found with query: %r'%query)
return
- workers = [Worker(url, result_queue, br, log, i, self) for i, url in
+ workers = [Worker(url, result_queue, br, log, i, domain, self) for i, url in
enumerate(matches)]
for w in workers:
From 1395c6d5d814349102d23d9cdfb26af8689b4bb0 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 24 May 2011 15:48:27 -0600
Subject: [PATCH 07/30] EPUB Output: Change any white-space:pre declarations in
the CSS to pre-wrap to accomodate readers that cannot scroll horizontally.
Fixes #786722 (chm to epub conversion fails to reflow text in
tag)
---
src/calibre/ebooks/epub/output.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py
index 0ed6d7e222..bea90eeba8 100644
--- a/src/calibre/ebooks/epub/output.py
+++ b/src/calibre/ebooks/epub/output.py
@@ -413,6 +413,13 @@ class EPUBOutput(OutputFormatPlugin):
rule.style.removeProperty('margin-left')
# padding-left breaks rendering in webkit and gecko
rule.style.removeProperty('padding-left')
+ # Change whitespace:pre to pre-line to accommodate readers that
+ # cannot scroll horizontally
+ for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
+ style = rule.style
+ ws = style.getPropertyValue('white-space')
+ if ws == 'pre':
+ style.setProperty('white-space', 'pre-wrap')
# }}}
From 50c80a8505c395c43904d0139dbaf908628c7298 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Tue, 24 May 2011 19:34:04 -0400
Subject: [PATCH 08/30] Store: Fix some bugs. Add basic store chooser which
give information and allows enabling and disabling store plugins in a user
friendly manner.
---
src/calibre/customize/builtins.py | 4 +-
src/calibre/gui2/actions/store.py | 7 +
src/calibre/gui2/store/config/chooser.py | 18 +
.../gui2/store/config/chooser/__init__.py | 0
.../config/chooser/adv_search_builder.py | 131 ++++++
.../config/chooser/adv_search_builder.ui | 416 ++++++++++++++++++
.../store/config/chooser/chooser_dialog.py | 28 ++
.../store/config/chooser/chooser_widget.py | 35 ++
.../store/config/chooser/chooser_widget.ui | 87 ++++
.../gui2/store/config/chooser/models.py | 244 ++++++++++
.../gui2/store/config/chooser/results_view.py | 31 ++
.../gui2/store/config/search/__init__.py | 0
.../config/{ => search}/search_widget.py | 2 +-
.../config/{ => search}/search_widget.ui | 0
src/calibre/gui2/store/config/store.py | 2 +-
src/calibre/gui2/store/mobileread/models.py | 1 +
.../gui2/store/search/adv_search_builder.py | 2 +-
src/calibre/gui2/store/search/search.py | 2 +-
src/calibre/gui2/store/search/search.ui | 7 +-
19 files changed, 1009 insertions(+), 8 deletions(-)
create mode 100644 src/calibre/gui2/store/config/chooser.py
create mode 100644 src/calibre/gui2/store/config/chooser/__init__.py
create mode 100644 src/calibre/gui2/store/config/chooser/adv_search_builder.py
create mode 100644 src/calibre/gui2/store/config/chooser/adv_search_builder.ui
create mode 100644 src/calibre/gui2/store/config/chooser/chooser_dialog.py
create mode 100644 src/calibre/gui2/store/config/chooser/chooser_widget.py
create mode 100644 src/calibre/gui2/store/config/chooser/chooser_widget.ui
create mode 100644 src/calibre/gui2/store/config/chooser/models.py
create mode 100644 src/calibre/gui2/store/config/chooser/results_view.py
create mode 100644 src/calibre/gui2/store/config/search/__init__.py
rename src/calibre/gui2/store/config/{ => search}/search_widget.py (96%)
rename src/calibre/gui2/store/config/{ => search}/search_widget.ui (100%)
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index d9e8be00b5..5c90e5699b 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -1316,7 +1316,7 @@ class StoreOpenLibraryStore(StoreBase):
actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore'
drm_free_only = True
- headquarters = ['US']
+ headquarters = 'US'
formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT']
class StoreOReillyStore(StoreBase):
@@ -1381,7 +1381,7 @@ class StoreWoblinkStore(StoreBase):
actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore'
drm_free_only = False
- location = 'PL'
+ headquarters = 'PL'
formats = ['EPUB']
plugins += [
diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py
index c8507e851c..effe470359 100644
--- a/src/calibre/gui2/actions/store.py
+++ b/src/calibre/gui2/actions/store.py
@@ -34,6 +34,8 @@ class StoreAction(InterfaceAction):
self.store_list_menu = self.store_menu.addMenu(_('Stores'))
for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()):
self.store_list_menu.addAction(n, partial(self.open_store, p))
+ self.store_menu.addSeparator()
+ self.store_menu.addAction(_('Choose stores'), self.choose)
self.qaction.setMenu(self.store_menu)
def do_search(self):
@@ -107,6 +109,11 @@ class StoreAction(InterfaceAction):
query = 'author:"%s" title:"%s"' % (self._get_author(row), self._get_title(row))
self.search(query)
+ def choose(self):
+ from calibre.gui2.store.config.chooser.chooser_dialog import StoreChooserDialog
+ d = StoreChooserDialog(self.gui)
+ d.exec_()
+
def open_store(self, store_plugin):
self.show_disclaimer()
store_plugin.open(self.gui)
diff --git a/src/calibre/gui2/store/config/chooser.py b/src/calibre/gui2/store/config/chooser.py
new file mode 100644
index 0000000000..f5c40a18ae
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, John Schember '
+__docformat__ = 'restructuredtext en'
+
+'''
+Config widget access functions for enabling and disabling stores.
+'''
+
+def config_widget():
+ from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget
+ return StoreChooserWidget()
+
+def save_settings(config_widget):
+ pass
diff --git a/src/calibre/gui2/store/config/chooser/__init__.py b/src/calibre/gui2/store/config/chooser/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/calibre/gui2/store/config/chooser/adv_search_builder.py b/src/calibre/gui2/store/config/chooser/adv_search_builder.py
new file mode 100644
index 0000000000..7b519abcd1
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser/adv_search_builder.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, John Schember '
+__docformat__ = 'restructuredtext en'
+
+import re
+
+from PyQt4.Qt import (QDialog, QDialogButtonBox)
+
+from calibre.gui2.store.config.chooser.adv_search_builder_ui import Ui_Dialog
+from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
+
+class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
+
+ def __init__(self, parent):
+ QDialog.__init__(self, parent)
+ self.setupUi(self)
+
+ self.buttonBox.accepted.connect(self.advanced_search_button_pushed)
+ self.tab_2_button_box.accepted.connect(self.accept)
+ self.tab_2_button_box.rejected.connect(self.reject)
+ self.clear_button.clicked.connect(self.clear_button_pushed)
+ self.adv_search_used = False
+ self.mc = ''
+
+ self.tabWidget.setCurrentIndex(0)
+ self.tabWidget.currentChanged[int].connect(self.tab_changed)
+ self.tab_changed(0)
+
+ def tab_changed(self, idx):
+ if idx == 1:
+ self.tab_2_button_box.button(QDialogButtonBox.Ok).setDefault(True)
+ else:
+ self.buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
+
+ def advanced_search_button_pushed(self):
+ self.adv_search_used = True
+ self.accept()
+
+ def clear_button_pushed(self):
+ self.name_box.setText('')
+ self.description_box.setText('')
+ self.headquarters_box.setText('')
+ self.format_box.setText('')
+ self.enabled_combo.setIndex(0)
+ self.drm_combo.setIndex(0)
+
+ def tokens(self, raw):
+ phrases = re.findall(r'\s*".*?"\s*', raw)
+ for f in phrases:
+ raw = raw.replace(f, ' ')
+ phrases = [t.strip('" ') for t in phrases]
+ return ['"' + self.mc + t + '"' for t in phrases + [r.strip() for r in raw.split()]]
+
+ def search_string(self):
+ if self.adv_search_used:
+ return self.adv_search_string()
+ else:
+ return self.box_search_string()
+
+ def adv_search_string(self):
+ mk = self.matchkind.currentIndex()
+ if mk == CONTAINS_MATCH:
+ self.mc = ''
+ elif mk == EQUALS_MATCH:
+ self.mc = '='
+ else:
+ self.mc = '~'
+ all, any, phrase, none = map(lambda x: unicode(x.text()),
+ (self.all, self.any, self.phrase, self.none))
+ all, any, none = map(self.tokens, (all, any, none))
+ phrase = phrase.strip()
+ all = ' and '.join(all)
+ any = ' or '.join(any)
+ none = ' and not '.join(none)
+ ans = ''
+ if phrase:
+ ans += '"%s"'%phrase
+ if all:
+ ans += (' and ' if ans else '') + all
+ if none:
+ ans += (' and not ' if ans else 'not ') + none
+ if any:
+ ans += (' or ' if ans else '') + any
+ return ans
+
+ def token(self):
+ txt = unicode(self.text.text()).strip()
+ if txt:
+ if self.negate.isChecked():
+ txt = '!'+txt
+ tok = self.FIELDS[unicode(self.field.currentText())]+txt
+ if re.search(r'\s', tok):
+ tok = '"%s"'%tok
+ return tok
+
+ def box_search_string(self):
+ mk = self.matchkind.currentIndex()
+ if mk == CONTAINS_MATCH:
+ self.mc = ''
+ elif mk == EQUALS_MATCH:
+ self.mc = '='
+ else:
+ self.mc = '~'
+
+ ans = []
+ self.box_last_values = {}
+ name = unicode(self.name_box.text()).strip()
+ if name:
+ ans.append('name:"' + self.mc + name + '"')
+ description = unicode(self.description_box.text()).strip()
+ if description:
+ ans.append('description:"' + self.mc + description + '"')
+ headquarters = unicode(self.headquarters_box.text()).strip()
+ if headquarters:
+ ans.append('headquarters:"' + self.mc + headquarters + '"')
+ format = unicode(self.format_box.text()).strip()
+ if format:
+ ans.append('format:"' + self.mc + format + '"')
+ enabled = unicode(self.enabled_combo.currentText()).strip()
+ if enabled:
+ ans.append('enabled:' + enabled)
+ drm = unicode(self.drm_combo.currentText()).strip()
+ if drm:
+ ans.append('drm:' + drm)
+ if ans:
+ return ' and '.join(ans)
+ return ''
diff --git a/src/calibre/gui2/store/config/chooser/adv_search_builder.ui b/src/calibre/gui2/store/config/chooser/adv_search_builder.ui
new file mode 100644
index 0000000000..7d57321c72
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser/adv_search_builder.ui
@@ -0,0 +1,416 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 752
+ 472
+
+
+
+ Advanced Search
+
+
+
+ :/images/search.png:/images/search.png
+
+
+ -
+
+
+ &What kind of match to use:
+
+
+ matchkind
+
+
+
+ -
+
+
-
+
+ Contains: the word or phrase matches anywhere in the metadata field
+
+
+ -
+
+ Equals: the word or phrase must match the entire metadata field
+
+
+ -
+
+ Regular expression: the expression must match anywhere in the metadata field
+
+
+
+
+ -
+
+
+ 0
+
+
+
+ A&dvanced Search
+
+
+
-
+
+
+ Find entries that have...
+
+
+
-
+
+
-
+
+
+ &All these words:
+
+
+ all
+
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+ This exact &phrase:
+
+
+ all
+
+
+
+ -
+
+
+
+
+ -
+
+
-
+
+
+ &One or more of these words:
+
+
+ all
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+ But dont show entries that have...
+
+
+
-
+
+
-
+
+
+ Any of these &unwanted words:
+
+
+ all
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+ 16777215
+ 30
+
+
+
+ See the <a href="http://calibre-ebook.com/user_manual/gui.html#the-search-interface">User Manual</a> for more help
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+ Nam&e/Description ...
+
+
+ -
+
+
+ &Name:
+
+
+ name_box
+
+
+
+ -
+
+
+ Enter the title.
+
+
+
+ -
+
+
+ &Description:
+
+
+ description_box
+
+
+
+ -
+
+
+ &Headquarters:
+
+
+ headquarters_box
+
+
+
+ -
+
+
-
+
+
+ &Clear
+
+
+
+ -
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Search only in specific fields:
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ &Format:
+
+
+ format_box
+
+
+
+ -
+
+
+ -
+
+
+ Enabled:
+
+
+
+ -
+
+
+ DRM:
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+ true
+
+
+ -
+
+ false
+
+
+
+
+ -
+
+
-
+
+
+
+
+ -
+
+ true
+
+
+ -
+
+ false
+
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+
+
+
+
+ EnLineEdit
+ QLineEdit
+
+
+
+
+ all
+ phrase
+ any
+ none
+ buttonBox
+ name_box
+ description_box
+ headquarters_box
+ format_box
+ clear_button
+ tab_2_button_box
+ tabWidget
+ matchkind
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/calibre/gui2/store/config/chooser/chooser_dialog.py b/src/calibre/gui2/store/config/chooser/chooser_dialog.py
new file mode 100644
index 0000000000..c94796dc11
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser/chooser_dialog.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, John Schember '
+__docformat__ = 'restructuredtext en'
+
+from PyQt4.Qt import (QDialog, QDialogButtonBox, QVBoxLayout)
+
+from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget
+
+class StoreChooserDialog(QDialog):
+
+ def __init__(self, parent):
+ QDialog.__init__(self, parent)
+
+ self.setWindowTitle(_('Choose stores'))
+
+ button_box = QDialogButtonBox(QDialogButtonBox.Close)
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+ v = QVBoxLayout(self)
+ self.config_widget = StoreChooserWidget()
+ v.addWidget(self.config_widget)
+ v.addWidget(button_box)
+
+ self.resize(800, 600)
diff --git a/src/calibre/gui2/store/config/chooser/chooser_widget.py b/src/calibre/gui2/store/config/chooser/chooser_widget.py
new file mode 100644
index 0000000000..93630d69a7
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser/chooser_widget.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, John Schember '
+__docformat__ = 'restructuredtext en'
+
+from PyQt4.Qt import (QWidget, QIcon, QDialog)
+
+from calibre.gui2.store.config.chooser.adv_search_builder import AdvSearchBuilderDialog
+from calibre.gui2.store.config.chooser.chooser_widget_ui import Ui_Form
+
+class StoreChooserWidget(QWidget, Ui_Form):
+
+ def __init__(self):
+ QWidget.__init__(self)
+ self.setupUi(self)
+
+ self.adv_search_builder.setIcon(QIcon(I('search.png')))
+
+ self.search.clicked.connect(self.do_search)
+ self.adv_search_builder.clicked.connect(self.build_adv_search)
+ self.results_view.activated.connect(self.toggle_plugin)
+
+ def do_search(self):
+ self.results_view.model().search(unicode(self.query.text()))
+
+ def toggle_plugin(self, index):
+ self.results_view.model().toggle_plugin(index)
+
+ def build_adv_search(self):
+ adv = AdvSearchBuilderDialog(self)
+ if adv.exec_() == QDialog.Accepted:
+ self.query.setText(adv.search_string())
diff --git a/src/calibre/gui2/store/config/chooser/chooser_widget.ui b/src/calibre/gui2/store/config/chooser/chooser_widget.ui
new file mode 100644
index 0000000000..69117406b1
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser/chooser_widget.ui
@@ -0,0 +1,87 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 610
+ 553
+
+
+
+ Form
+
+
+ -
+
+
-
+
+
+ Query:
+
+
+
+ -
+
+
+ ...
+
+
+
+ -
+
+
+ -
+
+
+ Search
+
+
+
+
+
+ -
+
+
+ true
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+ ResultsView
+ QTreeView
+
+
+
+
+
+
diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py
new file mode 100644
index 0000000000..460b698878
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser/models.py
@@ -0,0 +1,244 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, John Schember '
+__docformat__ = 'restructuredtext en'
+
+from PyQt4.Qt import (Qt, QAbstractItemModel, QIcon, QVariant, QModelIndex)
+
+from calibre.gui2 import NONE
+from calibre.customize.ui import is_disabled, disable_plugin, enable_plugin
+from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
+ REGEXP_MATCH
+from calibre.utils.icu import sort_key
+from calibre.utils.search_query_parser import SearchQueryParser
+
+
+class Matches(QAbstractItemModel):
+
+ HEADERS = [_('Enabled'), _('Name'), _('No DRM'), _('Headquarters'), _('Formats')]
+ HTML_COLS = [1]
+
+ def __init__(self, plugins):
+ QAbstractItemModel.__init__(self)
+
+ self.NO_DRM_ICON = QIcon(I('ok.png'))
+
+ self.all_matches = plugins
+ self.matches = plugins
+ self.filter = ''
+ self.search_filter = SearchFilter(self.all_matches)
+
+ self.sort_col = 1
+ self.sort_order = Qt.AscendingOrder
+
+ def get_plugin(self, index):
+ row = index.row()
+ if row < len(self.matches):
+ return self.matches[row]
+ else:
+ return None
+
+ def search(self, filter):
+ self.filter = filter.strip()
+ if not self.filter:
+ self.matches = self.all_matches
+ else:
+ try:
+ self.matches = list(self.search_filter.parse(self.filter))
+ except:
+ self.matches = self.all_matches
+ self.layoutChanged.emit()
+ self.sort(self.sort_col, self.sort_order)
+
+ def toggle_plugin(self, index):
+ new_index = self.createIndex(index.row(), 0)
+ data = QVariant(is_disabled(self.get_plugin(index)))
+ self.setData(new_index, data, Qt.CheckStateRole)
+
+ def index(self, row, column, parent=QModelIndex()):
+ return self.createIndex(row, column)
+
+ def parent(self, index):
+ if not index.isValid() or index.internalId() == 0:
+ return QModelIndex()
+ return self.createIndex(0, 0)
+
+ def rowCount(self, *args):
+ return len(self.matches)
+
+ def columnCount(self, *args):
+ return len(self.HEADERS)
+
+ def headerData(self, section, orientation, role):
+ if role != Qt.DisplayRole:
+ return NONE
+ text = ''
+ if orientation == Qt.Horizontal:
+ if section < len(self.HEADERS):
+ text = self.HEADERS[section]
+ return QVariant(text)
+ else:
+ return QVariant(section+1)
+
+ def data(self, index, role):
+ row, col = index.row(), index.column()
+ result = self.matches[row]
+ if role in (Qt.DisplayRole, Qt.EditRole):
+ if col == 1:
+ return QVariant('%s
%s' % (result.name, result.description))
+ elif col == 3:
+ return QVariant(result.headquarters)
+ elif col == 4:
+ return QVariant(', '.join(result.formats).upper())
+ elif role == Qt.DecorationRole:
+ if col == 2:
+ if result.drm_free_only:
+ return QVariant(self.NO_DRM_ICON)
+ elif role == Qt.CheckStateRole:
+ if col == 0:
+ if is_disabled(result):
+ return Qt.Unchecked
+ return Qt.Checked
+ elif role == Qt.ToolTipRole:
+ return QVariant('%s
' % result.description)
+ return NONE
+
+ def setData(self, index, data, role):
+ if not index.isValid():
+ return False
+ row, col = index.row(), index.column()
+ if col == 0:
+ if data.toBool():
+ enable_plugin(self.get_plugin(index))
+ else:
+ disable_plugin(self.get_plugin(index))
+ self.dataChanged.emit(self.index(index.row(), 0), self.index(index.row(), self.columnCount() - 1))
+ return True
+
+ def flags(self, index):
+ if index.column() == 0:
+ return QAbstractItemModel.flags(self, index) | Qt.ItemIsUserCheckable
+ return QAbstractItemModel.flags(self, index)
+
+ def data_as_text(self, match, col):
+ text = ''
+ if col == 0:
+ text = 'b' if is_disabled(match) else 'a'
+ elif col == 1:
+ text = match.name
+ elif col == 2:
+ text = 'b' if match.drm else 'a'
+ elif col == 3:
+ text = match.headquarteres
+ return text
+
+ def sort(self, col, order, reset=True):
+ self.sort_col = col
+ self.sort_order = order
+ if not self.matches:
+ return
+ descending = order == Qt.DescendingOrder
+ self.matches.sort(None,
+ lambda x: sort_key(unicode(self.data_as_text(x, col))),
+ descending)
+ if reset:
+ self.reset()
+
+
+class SearchFilter(SearchQueryParser):
+
+ USABLE_LOCATIONS = [
+ 'all',
+ 'description',
+ 'drm',
+ 'enabled',
+ 'format',
+ 'formats',
+ 'headquarters',
+ 'name',
+ ]
+
+ def __init__(self, all_plugins=[]):
+ SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
+ self.srs = set(all_plugins)
+
+ def universal_set(self):
+ return self.srs
+
+ def get_matches(self, location, query):
+ location = location.lower().strip()
+ if 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 = {
+ 'description': lambda x: x.description.lower(),
+ 'drm': lambda x: not x.drm_free_only,
+ 'enabled': lambda x: not is_disabled(x),
+ 'format': lambda x: ','.join(x.formats).lower(),
+ 'headquarters': lambda x: x.headquarters.lower(),
+ 'name': lambda x : x.name.lower(),
+ }
+ q['formats'] = q['format']
+ for sr in self.srs:
+ for locvalue in locations:
+ accessor = q[locvalue]
+ if query == 'true':
+ if locvalue in ('drm', 'enabled'):
+ if accessor(sr) == True:
+ matches.add(sr)
+ elif accessor(sr) is not None:
+ matches.add(sr)
+ continue
+ if query == 'false':
+ if locvalue in ('drm', 'enabled'):
+ if accessor(sr) == False:
+ matches.add(sr)
+ elif accessor(sr) is None:
+ matches.add(sr)
+ continue
+ # this is bool, so can't match below
+ if locvalue in ('drm', 'enabled'):
+ 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 == 'name' and matchkind == EQUALS_MATCH:
+ m = CONTAINS_MATCH
+ else:
+ m = matchkind
+
+ if locvalue == 'format':
+ vals = accessor(sr).split(',')
+ else:
+ vals = [accessor(sr)]
+ if _match(query, vals, m):
+ matches.add(sr)
+ break
+ except ValueError: # Unicode errors
+ import traceback
+ traceback.print_exc()
+ return matches
+
+
\ No newline at end of file
diff --git a/src/calibre/gui2/store/config/chooser/results_view.py b/src/calibre/gui2/store/config/chooser/results_view.py
new file mode 100644
index 0000000000..52d7696e4f
--- /dev/null
+++ b/src/calibre/gui2/store/config/chooser/results_view.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, John Schember '
+__docformat__ = 'restructuredtext en'
+
+from PyQt4.Qt import (QTreeView, QSize)
+
+from calibre.customize.ui import store_plugins
+from calibre.gui2.metadata.single_download import RichTextDelegate
+from calibre.gui2.store.config.chooser.models import Matches
+
+class ResultsView(QTreeView):
+
+ def __init__(self, *args):
+ QTreeView.__init__(self,*args)
+
+ self._model = Matches([p for p in store_plugins()])
+ self.setModel(self._model)
+
+ self.setIconSize(QSize(24, 24))
+
+ self.rt_delegate = RichTextDelegate(self)
+
+ for i in self._model.HTML_COLS:
+ self.setItemDelegateForColumn(i, self.rt_delegate)
+
+ for i in xrange(self._model.columnCount()):
+ self.resizeColumnToContents(i)
diff --git a/src/calibre/gui2/store/config/search/__init__.py b/src/calibre/gui2/store/config/search/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/calibre/gui2/store/config/search_widget.py b/src/calibre/gui2/store/config/search/search_widget.py
similarity index 96%
rename from src/calibre/gui2/store/config/search_widget.py
rename to src/calibre/gui2/store/config/search/search_widget.py
index 43e911a432..b2e55d2ad1 100644
--- a/src/calibre/gui2/store/config/search_widget.py
+++ b/src/calibre/gui2/store/config/search/search_widget.py
@@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QWidget
from calibre.gui2 import JSONConfig
-from calibre.gui2.store.config.search_widget_ui import Ui_Form
+from calibre.gui2.store.config.search.search_widget_ui import Ui_Form
class StoreConfigWidget(QWidget, Ui_Form):
diff --git a/src/calibre/gui2/store/config/search_widget.ui b/src/calibre/gui2/store/config/search/search_widget.ui
similarity index 100%
rename from src/calibre/gui2/store/config/search_widget.ui
rename to src/calibre/gui2/store/config/search/search_widget.ui
diff --git a/src/calibre/gui2/store/config/store.py b/src/calibre/gui2/store/config/store.py
index ddc24870bd..852f602d08 100644
--- a/src/calibre/gui2/store/config/store.py
+++ b/src/calibre/gui2/store/config/store.py
@@ -11,7 +11,7 @@ Config widget access functions for configuring the store action.
'''
def config_widget():
- from calibre.gui2.store.config.search_widget import StoreConfigWidget
+ from calibre.gui2.store.config.search.search_widget import StoreConfigWidget
return StoreConfigWidget()
def save_settings(config_widget):
diff --git a/src/calibre/gui2/store/mobileread/models.py b/src/calibre/gui2/store/mobileread/models.py
index a080affb51..297707e248 100644
--- a/src/calibre/gui2/store/mobileread/models.py
+++ b/src/calibre/gui2/store/mobileread/models.py
@@ -47,6 +47,7 @@ class BooksModel(QAbstractItemModel):
self.books = list(self.search_filter.parse(self.filter))
except:
self.books = self.all_books
+ self.layoutChanged.emit()
self.sort(self.sort_col, self.sort_order)
self.total_changed.emit(self.rowCount())
diff --git a/src/calibre/gui2/store/search/adv_search_builder.py b/src/calibre/gui2/store/search/adv_search_builder.py
index 50d4d3f3f4..745e709f90 100644
--- a/src/calibre/gui2/store/search/adv_search_builder.py
+++ b/src/calibre/gui2/store/search/adv_search_builder.py
@@ -116,7 +116,7 @@ class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
if price:
ans.append('price:"' + self.mc + price + '"')
format = unicode(self.format_box.text()).strip()
- if author:
+ if format:
ans.append('format:"' + self.mc + format + '"')
if ans:
return ' and '.join(ans)
diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py
index c7c252034d..ffc6ec097e 100644
--- a/src/calibre/gui2/store/search/search.py
+++ b/src/calibre/gui2/store/search/search.py
@@ -14,7 +14,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox,
from calibre.gui2 import JSONConfig, info_dialog
from calibre.gui2.progress_indicator import ProgressIndicator
-from calibre.gui2.store.config.search_widget import StoreConfigWidget
+from calibre.gui2.store.config.search.search_widget import StoreConfigWidget
from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog
from calibre.gui2.store.search.download_thread import SearchThreadPool, \
CacheUpdateThreadPool
diff --git a/src/calibre/gui2/store/search/search.ui b/src/calibre/gui2/store/search/search.ui
index 0360fa5f98..1451aa09f1 100644
--- a/src/calibre/gui2/store/search/search.ui
+++ b/src/calibre/gui2/store/search/search.ui
@@ -82,8 +82,8 @@
0
0
- 102
- 129
+ 125
+ 127
@@ -159,6 +159,9 @@
false
+
+ false
+
-
From b871315864d5e39a9f9e768b37d5268387decf73 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Tue, 24 May 2011 19:45:20 -0400
Subject: [PATCH 09/30] Store: Chooser, set sort and sort indicator.
---
src/calibre/gui2/store/config/chooser/results_view.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/calibre/gui2/store/config/chooser/results_view.py b/src/calibre/gui2/store/config/chooser/results_view.py
index 52d7696e4f..1c18a18d7b 100644
--- a/src/calibre/gui2/store/config/chooser/results_view.py
+++ b/src/calibre/gui2/store/config/chooser/results_view.py
@@ -6,7 +6,7 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember '
__docformat__ = 'restructuredtext en'
-from PyQt4.Qt import (QTreeView, QSize)
+from PyQt4.Qt import (Qt, QTreeView, QSize)
from calibre.customize.ui import store_plugins
from calibre.gui2.metadata.single_download import RichTextDelegate
@@ -29,3 +29,6 @@ class ResultsView(QTreeView):
for i in xrange(self._model.columnCount()):
self.resizeColumnToContents(i)
+
+ self.model().sort(1, Qt.AscendingOrder)
+ self.header().setSortIndicator(self.model().sort_col, self.model().sort_order)
From 41cc4be9528bfd8835e1f0b67bc6aff494d579f1 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Wed, 25 May 2011 06:55:52 -0400
Subject: [PATCH 10/30] Store: Reload store plugins accessible by GUI after
using the store chooser.
---
src/calibre/gui2/actions/store.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py
index effe470359..0fd783f0a3 100644
--- a/src/calibre/gui2/actions/store.py
+++ b/src/calibre/gui2/actions/store.py
@@ -113,6 +113,8 @@ class StoreAction(InterfaceAction):
from calibre.gui2.store.config.chooser.chooser_dialog import StoreChooserDialog
d = StoreChooserDialog(self.gui)
d.exec_()
+ self.gui.load_store_plugins()
+ self.load_menu()
def open_store(self, store_plugin):
self.show_disclaimer()
From a8deb0ed7d2b349a82711f96cb0ed28a3db16156 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 25 May 2011 09:56:27 -0600
Subject: [PATCH 11/30] ...
---
src/calibre/devices/android/driver.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index db473a755e..1cdf394c24 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -59,7 +59,7 @@ class ANDROID(USBMS):
0x0489 : { 0xc001 : [0x0226], 0xc004 : [0x0226], },
# Acer
- 0x502 : { 0x3203 : [0x0100]},
+ 0x502 : { 0x3203 : [0x0100, 0x224]},
# Dell
0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]},
From ce6cedf730c2c59b0cc41bcce7c29a969edf2eaf Mon Sep 17 00:00:00 2001
From: Li Fanxi
Date: Thu, 26 May 2011 00:24:04 +0800
Subject: [PATCH 12/30] [Bug] Error in passing API key to douban API.
---
src/calibre/ebooks/metadata/sources/douban.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/metadata/sources/douban.py b/src/calibre/ebooks/metadata/sources/douban.py
index 3c6bb7b6c7..8a95c4ed6b 100644
--- a/src/calibre/ebooks/metadata/sources/douban.py
+++ b/src/calibre/ebooks/metadata/sources/douban.py
@@ -211,7 +211,7 @@ class Douban(Source):
'q': q,
})
if self.DOUBAN_API_KEY and self.DOUBAN_API_KEY != '':
- url = url + "?apikey=" + self.DOUBAN_API_KEY
+ url = url + "&apikey=" + self.DOUBAN_API_KEY
return url
# }}}
From 70d1f3c046913e90cfd649859b2a2d32528a88b9 Mon Sep 17 00:00:00 2001
From: Li Fanxi
Date: Thu, 26 May 2011 00:31:47 +0800
Subject: [PATCH 13/30] [Bug] Error in passing API key to douban API when
search by isbn or douban id
---
src/calibre/ebooks/metadata/sources/douban.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/metadata/sources/douban.py b/src/calibre/ebooks/metadata/sources/douban.py
index 8a95c4ed6b..70bf01a00e 100644
--- a/src/calibre/ebooks/metadata/sources/douban.py
+++ b/src/calibre/ebooks/metadata/sources/douban.py
@@ -211,7 +211,10 @@ class Douban(Source):
'q': q,
})
if self.DOUBAN_API_KEY and self.DOUBAN_API_KEY != '':
- url = url + "&apikey=" + self.DOUBAN_API_KEY
+ if t == "isbn" or t == "subject":
+ url = url + "?apikey=" + self.DOUBAN_API_KEY
+ else:
+ url = url + "&apikey=" + self.DOUBAN_API_KEY
return url
# }}}
From b707ae28202b550a704c0729b040e254fcaca51e Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 25 May 2011 17:44:47 +0100
Subject: [PATCH 14/30] Addition of boolean template functions and, or, not.
Change documentation to include them. Add a function classification summary
to the documentation.
---
src/calibre/manual/template_lang.rst | 20 +++++++++-
src/calibre/utils/formatter_functions.py | 51 ++++++++++++++++++++++++
2 files changed, 70 insertions(+), 1 deletion(-)
diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst
index 69c77e5bfd..16a90f7531 100644
--- a/src/calibre/manual/template_lang.rst
+++ b/src/calibre/manual/template_lang.rst
@@ -229,13 +229,14 @@ For various values of series_index, the program returns:
The following functions are available in addition to those described in single-function mode. Remember from the example above that the single-function mode functions require an additional first parameter specifying the field to operate on. With the exception of the ``id`` parameter of assign, all parameters can be statements (sequences of expressions):
+ * ``and(value, value, ...)`` -- returns the string "1" if all values are not empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want.
* ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers.
* ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression
* ``booksize()`` -- returns the value of the |app| 'size' field. Returns '' if there are no formats.
* ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
* ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers.
* ``field(name)`` -- returns the metadata field named by ``name``.
- * ``first_non_empty(value, value, ...) -- returns the first value that is not empty. If all values are empty, then the empty value is returned. You can have as many values as you want.
+ * ``first_non_empty(value, value, ...)`` -- returns the first value that is not empty. If all values are empty, then the empty value is returned. You can have as many values as you want.
* ``format_date(x, date_format)`` -- format_date(val, format_string) -- format the value, which must be a date field, using the format_string, returning a string. The formatting codes are::
d : the day as number without a leading zero (1 to 31)
@@ -251,7 +252,9 @@ The following functions are available in addition to those described in single-f
iso : the date with time and timezone. Must be the only format present.
* ``eval(string)`` -- evaluates the string as a program, passing the local variables (those ``assign`` ed to). This permits using the template processor to construct complex results from local variables.
+ * ``not(value)`` -- returns the string "1" if the value is empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want.
* ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers.
+ * ``or(value, value, ...)`` -- returns the string "1" if any value is not empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want.
* ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole.
* ``raw_field(name)`` -- returns the metadata field named by name without applying any formatting.
* ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments.
@@ -259,7 +262,22 @@ The following functions are available in addition to those described in single-f
* ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
* ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers.
* ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value.
+
+Function classification summary:
+ * Get values from metadata: ``field``. ``raw_field``. In some situations, ``lookup`` can be used in place of ``field``.
+ * Arithmetic: ``add``, ``subtract``, ``multiply``, ``divide``
+ * Boolean: ``and``, ``or``, ``not``. The function ``if_empty`` is similar to ``and`` called with one argument.
+ * If-then-else: ``contains``, ``test``
+ * Iterating over values: ``first_non_empty``, ``lookup``, ``switch``
+ * List lookup: ``in_list``, ``list_item``, ``select``,
+ * List manipulation: ``count``, ``sublist``, ``subitems``
+ * Recursion: ``eval``, ``template``
+ * Relational: ``cmp`` , ``strcmp`` for strings
+ * String case changes: ``lowercase``, ``uppercase``, ``titlecase``, ``capitalize``
+ * String manipulation: ``re``, ``shorten``, ``substr``
+ * Other: ``assign``, ``booksize``, ``print``, ``format_date``,
+
.. _general_mode:
Using general program mode
diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py
index c53277f3ce..a3a156648f 100644
--- a/src/calibre/utils/formatter_functions.py
+++ b/src/calibre/utils/formatter_functions.py
@@ -594,7 +594,56 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
i += 1
return ''
+class BuiltinAnd(BuiltinFormatterFunction):
+ name = 'and'
+ arg_count = -1
+ doc = _('and(value, value, ...) -- '
+ 'returns the string "1" if all values are not empty, otherwise '
+ 'returns the empty string. This function works well with test or '
+ 'first_non_empty. You can have as many values as you want.')
+
+ def evaluate(self, formatter, kwargs, mi, locals, *args):
+ i = 0
+ while i < len(args):
+ if not args[i]:
+ return ''
+ i += 1
+ return '1'
+
+class BuiltinOr(BuiltinFormatterFunction):
+ name = 'or'
+ arg_count = -1
+ doc = _('or(value, value, ...) -- '
+ 'returns the string "1" if any value is not empty, otherwise '
+ 'returns the empty string. This function works well with test or '
+ 'first_non_empty. You can have as many values as you want.')
+
+ def evaluate(self, formatter, kwargs, mi, locals, *args):
+ i = 0
+ while i < len(args):
+ if args[i]:
+ return '1'
+ i += 1
+ return ''
+
+class BuiltinNot(BuiltinFormatterFunction):
+ name = 'not'
+ arg_count = 1
+ doc = _('not(value) -- '
+ 'returns the string "1" if the value is empty, otherwise '
+ 'returns the empty string. This function works well with test or '
+ 'first_non_empty. You can have as many values as you want.')
+
+ def evaluate(self, formatter, kwargs, mi, locals, *args):
+ i = 0
+ while i < len(args):
+ if args[i]:
+ return '1'
+ i += 1
+ return ''
+
builtin_add = BuiltinAdd()
+builtin_and = BuiltinAnd()
builtin_assign = BuiltinAssign()
builtin_booksize = BuiltinBooksize()
builtin_capitalize = BuiltinCapitalize()
@@ -612,6 +661,8 @@ builtin_list_item = BuiltinListitem()
builtin_lookup = BuiltinLookup()
builtin_lowercase = BuiltinLowercase()
builtin_multiply = BuiltinMultiply()
+builtin_not = BuiltinNot()
+builtin_or = BuiltinOr()
builtin_print = BuiltinPrint()
builtin_raw_field = BuiltinRaw_field()
builtin_re = BuiltinRe()
From aa30d964db0b8fbacfb963cfce3fd08cb30759cd Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Wed, 25 May 2011 18:09:41 +0100
Subject: [PATCH 15/30] Slight improvement to user device faq entry
---
src/calibre/manual/faq.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index d3784eda6f..b120fd4a1b 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -138,7 +138,7 @@ Follow these steps to find the problem:
My device is non-standard or unusual. What can I do to connect to it?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that presents that shows up as a disk drive in your operating system. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information.
+In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that shows up as a disk drive in your operating system. Note: on windows, the device must have a drive letter for calibre to use it. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information.
How does |app| manage collections on my SONY reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
From e1cf8e63602d9f412997d57e658b3c30a70b0546 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?=
Date: Wed, 25 May 2011 21:33:30 +0200
Subject: [PATCH 16/30] Virtualo Store
---
src/calibre/customize/builtins.py | 11 ++++
src/calibre/gui2/store/virtualo_plugin.py | 72 +++++++++++++++++++++++
2 files changed, 83 insertions(+)
create mode 100644 src/calibre/gui2/store/virtualo_plugin.py
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index d9e8be00b5..ac5377700e 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -1346,6 +1346,16 @@ class StoreSmashwordsStore(StoreBase):
headquarters = 'US'
formats = ['EPUB', 'HTML', 'LRF', 'MOBI', 'PDB', 'RTF', 'TXT']
+class StoreVirtualoStore(StoreBase):
+ name = 'Virtualo'
+ author = 'Tomasz Długosz'
+ description = u'Księgarnia internetowa, która oferuje bezpieczny i szeroki dostęp do książek w formie cyfrowej.'
+ actual_plugin = 'calibre.gui2.store.virtualo_plugin:VirtualoStore'
+
+ drm_free_only = False
+ location = 'PL'
+ formats = ['EPUB', 'PDF']
+
class StoreWaterstonesUKStore(StoreBase):
name = 'Waterstones UK'
author = 'Charles Haley'
@@ -1411,6 +1421,7 @@ plugins += [
StoreOReillyStore,
StorePragmaticBookshelfStore,
StoreSmashwordsStore,
+ StoreVirtualoStore,
StoreWaterstonesUKStore,
StoreWeightlessBooksStore,
StoreWizardsTowerBooksStore,
diff --git a/src/calibre/gui2/store/virtualo_plugin.py b/src/calibre/gui2/store/virtualo_plugin.py
new file mode 100644
index 0000000000..b1fe0091b6
--- /dev/null
+++ b/src/calibre/gui2/store/virtualo_plugin.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, Tomasz Długosz '
+__docformat__ = 'restructuredtext en'
+
+import re
+import urllib
+from contextlib import closing
+
+from lxml import html
+
+from PyQt4.Qt import QUrl
+
+from calibre import browser, url_slash_cleaner
+from calibre.gui2 import open_url
+from calibre.gui2.store import StorePlugin
+from calibre.gui2.store.basic_config import BasicStoreConfig
+from calibre.gui2.store.search_result import SearchResult
+from calibre.gui2.store.web_store_dialog import WebStoreDialog
+
+class VirtualoStore(BasicStoreConfig, StorePlugin):
+
+ def open(self, parent=None, detail_item=None, external=False):
+ url = 'http://virtualo.pl/ebook/c2/'
+ detail_url = None
+
+ if external or self.config.get('open_external', False):
+ open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
+ else:
+ d = WebStoreDialog(self.gui, url, parent, detail_url)
+ d.setWindowTitle(self.name)
+ d.set_tags(self.config.get('tags', ''))
+ d.exec_()
+
+ def search(self, query, max_results=10, timeout=60):
+ url = 'http://virtualo.pl/c2/?q=' + urllib.quote(query.encode('utf-8'))
+
+ br = browser()
+
+ counter = max_results
+ with closing(br.open(url, timeout=timeout)) as f:
+ doc = html.fromstring(f.read())
+ for data in doc.xpath('//div[@id="product_list"]/div/div[@class="column"]'):
+ if counter <= 0:
+ break
+
+ id = ''.join(data.xpath('.//table/tr[2]/td[1]/a/@href'))
+ if not id:
+ continue
+
+ price = ''.join(data.xpath('.//span[@class="price"]/text() | .//span[@class="price abbr"]/text()'))
+ cover_url = ''.join(data.xpath('.//table/tr[2]/td[1]/a/img/@src'))
+ title = ''.join(data.xpath('.//div[@class="title"]/a/text()'))
+ author = ', '.join(data.xpath('.//div[@class="authors"]/a/text()'))
+ formats = ', '.join(data.xpath('.//span[@class="format"]/a/text()'))
+ formats = re.sub(r'(, )?ONLINE(, )?', '', formats)
+
+ counter -= 1
+
+ s = SearchResult()
+ s.cover_url = cover_url
+ s.title = title.strip() + ' ' + formats
+ s.author = author.strip()
+ s.price = price + ' zł'
+ s.detail_item = 'http://virtualo.pl' + id.strip()
+ s.formats = formats.upper().strip()
+ s.drm = SearchResult.DRM_LOCKED if formats == 'EPUB' else SearchResult.DRM_UNKNOWN
+
+ yield s
From df81def0cf452d5063567e5aae8cdccf5f2e1c1a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?=
Date: Wed, 25 May 2011 21:44:33 +0200
Subject: [PATCH 17/30] fix Virtualo headquarters
---
src/calibre/customize/builtins.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 5a56a89cf0..14565da152 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -1353,7 +1353,7 @@ class StoreVirtualoStore(StoreBase):
actual_plugin = 'calibre.gui2.store.virtualo_plugin:VirtualoStore'
drm_free_only = False
- location = 'PL'
+ headquarters = 'PL'
formats = ['EPUB', 'PDF']
class StoreWaterstonesUKStore(StoreBase):
From 75782ed4f2c0dec3f27a440f831f455bee8400dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?=
Date: Wed, 25 May 2011 21:56:08 +0200
Subject: [PATCH 18/30] fix headquarters in chooser
---
src/calibre/gui2/store/config/chooser/models.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py
index 460b698878..f76ca45a01 100644
--- a/src/calibre/gui2/store/config/chooser/models.py
+++ b/src/calibre/gui2/store/config/chooser/models.py
@@ -132,7 +132,7 @@ class Matches(QAbstractItemModel):
elif col == 2:
text = 'b' if match.drm else 'a'
elif col == 3:
- text = match.headquarteres
+ text = match.headquarters
return text
def sort(self, col, order, reset=True):
@@ -241,4 +241,4 @@ class SearchFilter(SearchQueryParser):
traceback.print_exc()
return matches
-
\ No newline at end of file
+
From 3734b12cc5a0d36d528bc2c4281d87c04bd1d231 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tomasz=20D=C5=82ugosz?=
Date: Wed, 25 May 2011 22:05:06 +0200
Subject: [PATCH 19/30] not all EPUBs have DRM in Virtualo
---
src/calibre/gui2/store/virtualo_plugin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/gui2/store/virtualo_plugin.py b/src/calibre/gui2/store/virtualo_plugin.py
index b1fe0091b6..d86aa3e0e5 100644
--- a/src/calibre/gui2/store/virtualo_plugin.py
+++ b/src/calibre/gui2/store/virtualo_plugin.py
@@ -67,6 +67,6 @@ class VirtualoStore(BasicStoreConfig, StorePlugin):
s.price = price + ' zł'
s.detail_item = 'http://virtualo.pl' + id.strip()
s.formats = formats.upper().strip()
- s.drm = SearchResult.DRM_LOCKED if formats == 'EPUB' else SearchResult.DRM_UNKNOWN
+ s.drm = SearchResult.DRM_UNKNOWN
yield s
From 7b98249b90366e26900d2b2cc5a86ff54595b083 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Wed, 25 May 2011 17:51:29 -0400
Subject: [PATCH 20/30] Store: chooser, fix sorting by drm.
---
src/calibre/gui2/store/config/chooser/models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py
index 0c784f6614..3ceed6fb00 100644
--- a/src/calibre/gui2/store/config/chooser/models.py
+++ b/src/calibre/gui2/store/config/chooser/models.py
@@ -130,7 +130,7 @@ class Matches(QAbstractItemModel):
elif col == 1:
text = match.name
elif col == 2:
- text = 'b' if getattr(match, 'drm', True) else 'a'
+ text = 'a' if getattr(match, 'drm_free_only', True) else 'b'
elif col == 3:
text = getattr(match, 'headquarters', '')
return text
From 47cd16b6656e3f830f67fabc36fd3169242293c3 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Wed, 25 May 2011 18:53:11 -0400
Subject: [PATCH 21/30] Store: ...
---
src/calibre/customize/builtins.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 14565da152..bbbb8a738f 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -1348,7 +1348,7 @@ class StoreSmashwordsStore(StoreBase):
class StoreVirtualoStore(StoreBase):
name = 'Virtualo'
- author = 'Tomasz Długosz'
+ author = u'Tomasz Długosz'
description = u'Księgarnia internetowa, która oferuje bezpieczny i szeroki dostęp do książek w formie cyfrowej.'
actual_plugin = 'calibre.gui2.store.virtualo_plugin:VirtualoStore'
@@ -1386,7 +1386,7 @@ class StoreWizardsTowerBooksStore(StoreBase):
class StoreWoblinkStore(StoreBase):
name = 'Woblink'
- author = 'Tomasz Długosz'
+ author = u'Tomasz Długosz'
description = u'Czytanie zdarza się wszędzie!'
actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore'
From c6e98f3af75ed803b50a83a4125bd014075a6ef1 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 25 May 2011 19:02:53 -0600
Subject: [PATCH 22/30] Dont allow user to use non email usernames when setting
up Hotmail or Gmail accounts
---
src/calibre/gui2/wizard/send_email.py | 108 +++++++++++++++-----------
src/calibre/manual/faq.rst | 2 +-
2 files changed, 63 insertions(+), 47 deletions(-)
diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py
index 5c7d916e1a..4337e558eb 100644
--- a/src/calibre/gui2/wizard/send_email.py
+++ b/src/calibre/gui2/wizard/send_email.py
@@ -46,6 +46,64 @@ class TestEmail(QDialog, TE_Dialog):
finally:
self.test_button.setEnabled(True)
+class RelaySetup(QDialog):
+
+ def __init__(self, service, parent):
+ QDialog.__init__(self, parent)
+
+ self.l = l = QGridLayout()
+ self.setLayout(l)
+ self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
+ bb.accepted.connect(self.accept)
+ bb.rejected.connect(self.reject)
+ self.tl = QLabel(('
'+_('Setup sending email using') +
+ ' {name}
' +
+ _('If you don\'t have an account, you can sign up for a free {name} email '
+ 'account at http://{url}. {extra}')).format(
+ **service))
+ l.addWidget(self.tl, 0, 0, 3, 0)
+ self.tl.setWordWrap(True)
+ self.tl.setOpenExternalLinks(True)
+ for name, label in (
+ ['from_', _('Your %s &email address:')],
+ ['username', _('Your %s &username:')],
+ ['password', _('Your %s &password:')],
+ ):
+ la = QLabel(label%service['name'])
+ le = QLineEdit(self)
+ setattr(self, name, le)
+ setattr(self, name+'_label', la)
+ r = l.rowCount()
+ l.addWidget(la, r, 0)
+ l.addWidget(le, r, 1)
+ la.setBuddy(le)
+ if name == 'password':
+ self.ptoggle = QCheckBox(_('&Show password'), self)
+ l.addWidget(self.ptoggle, r, 2)
+ self.ptoggle.stateChanged.connect(
+ lambda s: self.password.setEchoMode(self.password.Normal if s
+ == Qt.Checked else self.password.Password))
+ self.username.setText(service['username'])
+ self.password.setEchoMode(self.password.Password)
+ self.bl = QLabel('
' + _(
+ 'If you plan to use email to send books to your Kindle, remember to'
+ ' add the your %s email address to the allowed email addresses in your '
+ 'Amazon.com Kindle management page.')%service['name'])
+ self.bl.setWordWrap(True)
+ l.addWidget(self.bl, l.rowCount(), 0, 3, 0)
+ l.addWidget(bb, l.rowCount(), 0, 3, 0)
+ self.setWindowTitle(_('Setup') + ' ' + service['name'])
+ self.resize(self.sizeHint())
+ self.service = service
+
+ def accept(self):
+ un = unicode(self.username.text())
+ if self.service.get('at_in_username', False) and '@' not in un:
+ return error_dialog(self, _('Incorrect username'),
+ _('%s needs the full email address as your username') %
+ self.service['name'], show=True)
+ QDialog.accept(self)
+
class SendEmail(QWidget, Ui_Form):
@@ -129,7 +187,8 @@ class SendEmail(QWidget, Ui_Form):
'port': 587,
'username': '@gmail.com',
'url': 'www.gmail.com',
- 'extra': ''
+ 'extra': '',
+ 'at_in_username': True,
},
'hotmail': {
'name': 'Hotmail',
@@ -143,53 +202,10 @@ class SendEmail(QWidget, Ui_Form):
' will let calibre send email. In this case, I'
' strongly suggest you setup a free gmail account'
' instead.'),
+ 'at_in_username': True,
}
}[service]
- d = QDialog(self)
- l = QGridLayout()
- d.setLayout(l)
- bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
- bb.accepted.connect(d.accept)
- bb.rejected.connect(d.reject)
- d.tl = QLabel(('
'+_('Setup sending email using') +
- ' {name}
' +
- _('If you don\'t have an account, you can sign up for a free {name} email '
- 'account at http://{url}. {extra}')).format(
- **service))
- l.addWidget(d.tl, 0, 0, 3, 0)
- d.tl.setWordWrap(True)
- d.tl.setOpenExternalLinks(True)
- for name, label in (
- ['from_', _('Your %s &email address:')],
- ['username', _('Your %s &username:')],
- ['password', _('Your %s &password:')],
- ):
- la = QLabel(label%service['name'])
- le = QLineEdit(d)
- setattr(d, name, le)
- setattr(d, name+'_label', la)
- r = l.rowCount()
- l.addWidget(la, r, 0)
- l.addWidget(le, r, 1)
- la.setBuddy(le)
- if name == 'password':
- d.ptoggle = QCheckBox(_('&Show password'), d)
- l.addWidget(d.ptoggle, r, 2)
- d.ptoggle.stateChanged.connect(
- lambda s: d.password.setEchoMode(d.password.Normal if s
- == Qt.Checked else d.password.Password))
- d.username.setText(service['username'])
- d.password.setEchoMode(d.password.Password)
- d.bl = QLabel('
' + _(
- 'If you plan to use email to send books to your Kindle, remember to'
- ' add the your %s email address to the allowed email addresses in your '
- 'Amazon.com Kindle management page.')%service['name'])
- d.bl.setWordWrap(True)
- l.addWidget(d.bl, l.rowCount(), 0, 3, 0)
- l.addWidget(bb, l.rowCount(), 0, 3, 0)
- d.setWindowTitle(_('Setup') + ' ' + service['name'])
- d.resize(d.sizeHint())
- bb.setVisible(True)
+ d = RelaySetup(service, self)
if d.exec_() != d.Accepted:
return
self.relay_username.setText(d.username.text())
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index b120fd4a1b..1c0b49f30b 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -587,7 +587,7 @@ You can download news and convert it into an ebook with the command::
/opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub
-If you want to generate MOBI, use outputfile.mobi instead.
+If you want to generate MOBI, use outputfile.mobi instead and use ``--output-profile kindle``.
You can email downloaded news with the command::
From 4e6c543f75bbaf66a3fd46313c3d91762643b511 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 25 May 2011 19:07:58 -0600
Subject: [PATCH 23/30] Fix #788378 (Add support for nook TSR)
---
src/calibre/customize/builtins.py | 7 +++----
src/calibre/devices/nook/driver.py | 10 ++++++++++
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index bbbb8a738f..4a970b4661 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -594,7 +594,7 @@ from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
-from calibre.devices.nook.driver import NOOK, NOOK_COLOR
+from calibre.devices.nook.driver import NOOK, NOOK_COLOR, NOOK_TSR
from calibre.devices.prs505.driver import PRS505
from calibre.devices.user_defined.driver import USER_DEFINED
from calibre.devices.android.driver import ANDROID, S60
@@ -693,8 +693,7 @@ plugins += [
KINDLE,
KINDLE2,
KINDLE_DX,
- NOOK,
- NOOK_COLOR,
+ NOOK, NOOK_COLOR, NOOK_TSR,
PRS505,
ANDROID,
S60,
@@ -1277,7 +1276,7 @@ class StoreLegimiStore(StoreBase):
author = u'Tomasz Długosz'
description = u'Tanie oraz darmowe ebooki, egazety i blogi w formacie EPUB, wprost na Twój e-czytnik, iPhone, iPad, Android i komputer'
actual_plugin = 'calibre.gui2.store.legimi_plugin:LegimiStore'
-
+
drm_free_only = False
headquarters = 'PL'
formats = ['EPUB']
diff --git a/src/calibre/devices/nook/driver.py b/src/calibre/devices/nook/driver.py
index 39d0763735..3c30b88568 100644
--- a/src/calibre/devices/nook/driver.py
+++ b/src/calibre/devices/nook/driver.py
@@ -107,3 +107,13 @@ class NOOK_COLOR(NOOK):
return filepath
+class NOOK_TSR(NOOK):
+ gui_name = _('Nook Simple')
+ description = _('Communicate with the Nook TSR eBook reader.')
+
+ PRODUCT_ID = [0x003]
+ BCD = [0x216]
+
+ EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'My Files/Books'
+
+
From 612c4c36da96e53b772209e29682fdd1cf409b62 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Thu, 26 May 2011 07:06:15 -0400
Subject: [PATCH 24/30] Store: chooser, add descriptive tool tips.
---
src/calibre/gui2/store/config/chooser/models.py | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py
index 2eaa1655ce..6c95d74ffc 100644
--- a/src/calibre/gui2/store/config/chooser/models.py
+++ b/src/calibre/gui2/store/config/chooser/models.py
@@ -103,7 +103,22 @@ class Matches(QAbstractItemModel):
return Qt.Unchecked
return Qt.Checked
elif role == Qt.ToolTipRole:
- return QVariant('%s
' % result.description)
+ if col == 0:
+ if is_disabled(result):
+ return QVariant(_('This store is currently diabled and cannot be used in other parts of calibre.
'))
+ else:
+ return QVariant(_('This store is currently enabled and can be used in other parts of calibre.
'))
+ elif col == 1:
+ return QVariant('%s
' % result.description)
+ elif col == 2:
+ if result.drm_free_only:
+ return QVariant(_('This store only distributes ebooks with DRM.
'))
+ else:
+ return QVariant(_('This store distributes ebooks with DRM. It may have some titles without DRM, but you will need to check on a per title basis.
'))
+ elif col == 3:
+ return QVariant(_('This store is headquartered in %s. This is a good indication of what market the store caters to. However, this does not necessarily mean that the store is limited to that market only.
') % result.headquarters)
+ elif col == 4:
+ return QVariant(_('This store distributes ebooks in the following formats: %s
') % ', '.join(result.formats))
return NONE
def setData(self, index, data, role):
From 86ad6f7787cada352fc33633865389abbc35e32f Mon Sep 17 00:00:00 2001
From: John Schember
Date: Thu, 26 May 2011 08:29:27 -0400
Subject: [PATCH 25/30] Store: Search, use the GUI object directly to handle
istores instead of passing it to the dialog. This to allow for reloading of
enabled stores later.
---
src/calibre/gui2/actions/store.py | 2 +-
src/calibre/gui2/store/search/search.py | 86 +++++++++++--------------
2 files changed, 40 insertions(+), 48 deletions(-)
diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py
index 0fd783f0a3..6d9720548e 100644
--- a/src/calibre/gui2/actions/store.py
+++ b/src/calibre/gui2/actions/store.py
@@ -44,7 +44,7 @@ class StoreAction(InterfaceAction):
def search(self, query=''):
self.show_disclaimer()
from calibre.gui2.store.search.search import SearchDialog
- sd = SearchDialog(self.gui.istores, self.gui, query)
+ sd = SearchDialog(self.gui, self.gui, query)
sd.exec_()
def _get_selected_row(self):
diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py
index 3be39e3e87..fd49ebd67b 100644
--- a/src/calibre/gui2/store/search/search.py
+++ b/src/calibre/gui2/store/search/search.py
@@ -10,7 +10,7 @@ import re
from random import shuffle
from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox,
- QVBoxLayout, QIcon, QWidget)
+ QVBoxLayout, QIcon, QWidget, QTabWidget)
from calibre.gui2 import JSONConfig, info_dialog
from calibre.gui2.progress_indicator import ProgressIndicator
@@ -22,7 +22,7 @@ from calibre.gui2.store.search.search_ui import Ui_Dialog
class SearchDialog(QDialog, Ui_Dialog):
- def __init__(self, istores, parent=None, query=''):
+ def __init__(self, gui, parent=None, query=''):
QDialog.__init__(self, parent)
self.setupUi(self)
@@ -34,8 +34,7 @@ class SearchDialog(QDialog, Ui_Dialog):
# the variables it sets up are used later.
self.load_settings()
- # We keep a cache of store plugins and reference them by name.
- self.store_plugins = istores
+ self.gui = gui
# Setup our worker threads.
self.search_pool = SearchThreadPool(self.search_thread_count)
@@ -49,22 +48,11 @@ class SearchDialog(QDialog, Ui_Dialog):
self.hang_check = 0
# Update store caches silently.
- for p in self.store_plugins.values():
+ for p in self.gui.istores.values():
self.cache_pool.add_task(p, self.timeout)
- # Add check boxes for each store so the user
- # can disable searching specific stores on a
- # per search basis.
- stores_check_widget = QWidget()
- store_list_layout = QVBoxLayout()
- stores_check_widget.setLayout(store_list_layout)
- for x in sorted(self.store_plugins.keys(), key=lambda x: x.lower()):
- cbox = QCheckBox(x)
- cbox.setChecked(False)
- store_list_layout.addWidget(cbox)
- setattr(self, 'store_check_' + x, cbox)
- store_list_layout.addStretch()
- self.store_list.setWidget(stores_check_widget)
+ self.store_checks = {}
+ self.setup_store_checks()
# Set the search query
self.search_edit.setText(query)
@@ -91,6 +79,22 @@ class SearchDialog(QDialog, Ui_Dialog):
self.progress_checker.start(100)
self.restore_state()
+
+ def setup_store_checks(self):
+ # Add check boxes for each store so the user
+ # can disable searching specific stores on a
+ # per search basis.
+ stores_check_widget = QWidget()
+ store_list_layout = QVBoxLayout()
+ stores_check_widget.setLayout(store_list_layout)
+ for x in sorted(self.gui.istores.keys(), key=lambda x: x.lower()):
+ cbox = QCheckBox(x)
+ cbox.setChecked(False)
+ store_list_layout.addWidget(cbox)
+ self.store_checks['store_check_' + x] = cbox
+ store_list_layout.addStretch()
+ self.store_list.setWidget(stores_check_widget)
+
def build_adv_search(self):
adv = AdvSearchBuilderDialog(self)
@@ -126,11 +130,12 @@ class SearchDialog(QDialog, Ui_Dialog):
# futher filtering.
self.results_view.model().set_query(query)
- # Plugins are in alphebetic order. Randomize the
- # order of plugin names. This way plugins closer
+ # Plugins are in random order that does not change.
+ # Randomize the ord of the plugin names every time
+ # there is a search. This way plugins closer
# to a don't have an unfair advantage over
# plugins further from a.
- store_names = self.store_plugins.keys()
+ store_names = self.store_checks.keys()
if not store_names:
return
# Remove all of our internal filtering logic from the query.
@@ -138,8 +143,8 @@ class SearchDialog(QDialog, Ui_Dialog):
shuffle(store_names)
# Add plugins that the user has checked to the search pool's work queue.
for n in store_names:
- if getattr(self, 'store_check_' + n).isChecked():
- self.search_pool.add_task(query, n, self.store_plugins[n], self.max_results, self.timeout)
+ if self.store_checks[n].isChecked():
+ self.search_pool.add_task(query, n, self.gui.istores[n], self.max_results, self.timeout)
self.hang_check = 0
self.checker.start(100)
self.pi.startAnimation()
@@ -179,8 +184,8 @@ class SearchDialog(QDialog, Ui_Dialog):
self.config['open_external'] = self.open_external.isChecked()
store_check = {}
- for n in self.store_plugins:
- store_check[n] = getattr(self, 'store_check_' + n).isChecked()
+ for k, v in self.store_checks.items():
+ store_check[k] = v.isChecked()
self.config['store_checked'] = store_check
def restore_state(self):
@@ -206,8 +211,8 @@ class SearchDialog(QDialog, Ui_Dialog):
store_check = self.config.get('store_checked', None)
if store_check:
for n in store_check:
- if hasattr(self, 'store_check_' + n):
- getattr(self, 'store_check_' + n).setChecked(store_check[n])
+ if n in self.store_checks:
+ self.store_checks[n].setChecked(store_check[n])
self.results_view.model().sort_col = self.config.get('sort_col', 2)
self.results_view.model().sort_order = self.config.get('sort_order', Qt.AscendingOrder)
@@ -234,7 +239,7 @@ class SearchDialog(QDialog, Ui_Dialog):
self.config['open_external'] = self.open_external.isChecked()
d = QDialog(self)
- button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ button_box = QDialogButtonBox(QDialogButtonBox.Close)
v = QVBoxLayout(d)
button_box.accepted.connect(d.accept)
button_box.rejected.connect(d.reject)
@@ -244,10 +249,8 @@ class SearchDialog(QDialog, Ui_Dialog):
v.addWidget(button_box)
d.exec_()
-
- if d.result() == QDialog.Accepted:
- config_widget.save_settings()
- self.config_changed()
+ config_widget.save_settings()
+ self.config_changed()
def config_changed(self):
self.load_settings()
@@ -283,7 +286,7 @@ class SearchDialog(QDialog, Ui_Dialog):
def open_store(self, index):
result = self.results_view.model().get_result(index)
- self.store_plugins[result.store_name].open(self, result.detail_item, self.open_external.isChecked())
+ self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked())
def check_progress(self):
if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running():
@@ -292,27 +295,16 @@ class SearchDialog(QDialog, Ui_Dialog):
if not self.pi.isAnimated():
self.pi.startAnimation()
- def get_store_checks(self):
- '''
- Returns a list of QCheckBox's for each store.
- '''
- checks = []
- for x in self.store_plugins:
- check = getattr(self, 'store_check_' + x, None)
- if check:
- checks.append(check)
- return checks
-
def stores_select_all(self):
- for check in self.get_store_checks():
+ for check in self.store_checks.values():
check.setChecked(True)
def stores_select_invert(self):
- for check in self.get_store_checks():
+ for check in self.store_checks.values():
check.setChecked(not check.isChecked())
def stores_select_none(self):
- for check in self.get_store_checks():
+ for check in self.store_checks.values():
check.setChecked(False)
def dialog_closed(self, result):
From 0a18bb39060df5c20a93f433e3ddebc6b4766f00 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Thu, 26 May 2011 08:45:04 -0400
Subject: [PATCH 26/30] Store: Search, integrate store chooser into config
dialog.
---
src/calibre/gui2/store/search/search.py | 27 +++++++++++++++++++------
1 file changed, 21 insertions(+), 6 deletions(-)
diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py
index fd49ebd67b..faeaf507c9 100644
--- a/src/calibre/gui2/store/search/search.py
+++ b/src/calibre/gui2/store/search/search.py
@@ -14,6 +14,7 @@ from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox,
from calibre.gui2 import JSONConfig, info_dialog
from calibre.gui2.progress_indicator import ProgressIndicator
+from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget
from calibre.gui2.store.config.search.search_widget import StoreConfigWidget
from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog
from calibre.gui2.store.search.download_thread import SearchThreadPool, \
@@ -84,18 +85,23 @@ class SearchDialog(QDialog, Ui_Dialog):
# Add check boxes for each store so the user
# can disable searching specific stores on a
# per search basis.
+ existing = {}
+ for n in self.store_checks:
+ existing[n] = self.store_checks[n].isChecked()
+
+ self.store_checks = {}
+
stores_check_widget = QWidget()
store_list_layout = QVBoxLayout()
stores_check_widget.setLayout(store_list_layout)
for x in sorted(self.gui.istores.keys(), key=lambda x: x.lower()):
cbox = QCheckBox(x)
- cbox.setChecked(False)
+ cbox.setChecked(existing.get(x, False))
store_list_layout.addWidget(cbox)
- self.store_checks['store_check_' + x] = cbox
+ self.store_checks[x] = cbox
store_list_layout.addStretch()
self.store_list.setWidget(stores_check_widget)
-
def build_adv_search(self):
adv = AdvSearchBuilderDialog(self)
if adv.exec_() == QDialog.Accepted:
@@ -244,13 +250,22 @@ class SearchDialog(QDialog, Ui_Dialog):
button_box.accepted.connect(d.accept)
button_box.rejected.connect(d.reject)
d.setWindowTitle(_('Customize get books search'))
- config_widget = StoreConfigWidget(self.config)
- v.addWidget(config_widget)
+
+ tab_widget = QTabWidget(d)
+ v.addWidget(tab_widget)
v.addWidget(button_box)
+ chooser_config_widget = StoreChooserWidget()
+ search_config_widget = StoreConfigWidget(self.config)
+
+ tab_widget.addTab(chooser_config_widget, _('Choose stores'))
+ tab_widget.addTab(search_config_widget, _('Configure search'))
+
d.exec_()
- config_widget.save_settings()
+ search_config_widget.save_settings()
self.config_changed()
+ self.gui.load_store_plugins()
+ self.setup_store_checks()
def config_changed(self):
self.load_settings()
From ea0920053f9c89909b70ebaf084b9b0fc0ea7950 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 26 May 2011 10:50:57 -0600
Subject: [PATCH 27/30] Fix #774849 (Schedule news download: inconsistent save
behaviour)
---
src/calibre/gui2/dialogs/scheduler.py | 18 +++---
src/calibre/gui2/dialogs/scheduler.ui | 79 ++++++++++++++++-----------
2 files changed, 55 insertions(+), 42 deletions(-)
diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py
index b25d66979d..7d1d87b472 100644
--- a/src/calibre/gui2/dialogs/scheduler.py
+++ b/src/calibre/gui2/dialogs/scheduler.py
@@ -207,8 +207,9 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.recipe_model.searched.connect(self.search.search_done,
type=Qt.QueuedConnection)
self.recipe_model.searched.connect(self.search_done)
- self.search.setFocus(Qt.OtherFocusReason)
+ self.recipes.setFocus(Qt.OtherFocusReason)
self.commit_on_change = True
+ self.previous_urn = None
self.recipes.setModel(self.recipe_model)
self.detail_box.setVisible(False)
@@ -228,6 +229,9 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.old_news.setValue(gconf['oldest_news'])
+ self.go_button.clicked.connect(self.search.do_search)
+ self.clear_search_button.clicked.connect(self.search.clear_clicked)
+
def set_pw_echo_mode(self, state):
self.password.setEchoMode(self.password.Normal
if state == Qt.Checked else self.password.Password)
@@ -265,14 +269,9 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.last_downloaded.setVisible(enabled)
def current_changed(self, current, previous):
- if self.commit_on_change:
- if previous.isValid():
- if not self.commit(urn=getattr(previous.internalPointer(),
- 'urn', None)):
- self.commit_on_change = False
- self.recipes.setCurrentIndex(previous)
- else:
- self.commit_on_change = True
+ if self.previous_urn is not None:
+ self.commit(urn=self.previous_urn)
+ self.previous_urn = None
urn = self.current_urn
if urn is not None:
@@ -332,6 +331,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
return True
def initialize_detail_box(self, urn):
+ self.previous_urn = urn
self.detail_box.setVisible(True)
self.download_button.setVisible(True)
self.detail_box.setCurrentIndex(0)
diff --git a/src/calibre/gui2/dialogs/scheduler.ui b/src/calibre/gui2/dialogs/scheduler.ui
index f26bfc7285..6acbb01dd8 100644
--- a/src/calibre/gui2/dialogs/scheduler.ui
+++ b/src/calibre/gui2/dialogs/scheduler.ui
@@ -17,21 +17,30 @@
:/images/scheduler.png:/images/scheduler.png
-
+
-
-
-
- &Search:
-
-
- search
-
-
+
+
-
+
+
+ -
+
+
+ Go
+
+
+
+ -
+
+
+
+ :/images/clear_left.png:/images/clear_left.png
+
+
+
+
- -
-
-
- -
+
-
QFrame::NoFrame
@@ -44,7 +53,7 @@
0
0
- 486
+ 524
504
@@ -320,7 +329,7 @@
- -
+
-
@@ -345,7 +354,17 @@
- -
+
-
+
+
+
+
+
+ Qt::AlignCenter
+
+
+
+ -
-
@@ -376,17 +395,7 @@
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Save
-
-
-
- -
+
-
Download all scheduled news sources at once
@@ -394,15 +403,19 @@
Download &all scheduled
+
+
+ :/images/news.png:/images/news.png
+
- -
-
-
-
+
-
+
+
+ Qt::Horizontal
-
- Qt::AlignCenter
+
+ QDialogButtonBox::Save
From 5744242319e151fd7ac6292afb2eef9967520cca Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 26 May 2011 10:58:48 -0600
Subject: [PATCH 28/30] Fix #788570 (Calibre Epub to Mobi does not handle
hidden spans correctly)
---
src/calibre/ebooks/mobi/mobiml.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py
index 3c36a6166d..2275552c15 100644
--- a/src/calibre/ebooks/mobi/mobiml.py
+++ b/src/calibre/ebooks/mobi/mobiml.py
@@ -297,9 +297,11 @@ class MobiMLizer(object):
if id_:
# Keep anchors so people can use display:none
# to generate hidden TOCs
+ tail = elem.tail
elem.clear()
elem.text = None
elem.set('id', id_)
+ elem.tail = tail
else:
return
tag = barename(elem.tag)
@@ -309,7 +311,8 @@ class MobiMLizer(object):
istates.append(istate)
left = 0
display = style['display']
- isblock = not display.startswith('inline')
+ isblock = (not display.startswith('inline') and style['display'] !=
+ 'none')
isblock = isblock and style['float'] == 'none'
isblock = isblock and tag != 'br'
if isblock:
From 87dec0e0640fb1013c55b0a2f2debad7963139d2 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 26 May 2011 11:46:35 -0600
Subject: [PATCH 29/30] Update faz.net
---
recipes/faznet.recipe | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
diff --git a/recipes/faznet.recipe b/recipes/faznet.recipe
index 01a46d43ba..50a66c59b8 100644
--- a/recipes/faznet.recipe
+++ b/recipes/faznet.recipe
@@ -3,10 +3,7 @@ class AdvancedUserRecipe1303841067(BasicNewsRecipe):
title = u'Faz.net'
__author__ = 'schuster'
- remove_tags = [dict(attrs={'class':['right', 'ArrowLinkRight', 'ModulVerlagsInfo', 'left', 'Head']}),
- dict(id=['BreadCrumbs', 'tstag', 'FazFooterPrint']),
- dict(name=['script', 'noscript', 'style'])]
- oldest_article = 2
+ oldest_article = 1
description = 'Frankfurter Allgemeine Zeitung'
max_articles_per_feed = 100
no_stylesheets = True
@@ -15,9 +12,9 @@ class AdvancedUserRecipe1303841067(BasicNewsRecipe):
remove_javascript = True
cover_url = 'http://www.faz.net/f30/Images/Logos/logo.gif'
- def print_version(self, url):
- return url.replace('.html', '~Afor~Eprint.html')
-
+ remove_tags = [dict(attrs={'class':['LinkBoxModulSmall', 'ModulLesermeinungenFooter', 'ModulArtikelServices', 'SocialMediaUnten', 'ArrowLinkRight', 'ModulVerlagsInfo', 'AdData', 'FazFooter', 'Date']}),
+ dict(id=['FAZNavHeader', 'FAZNavMain', 'RightColumn', 'FazFooter', 'BreadCrumbs', 'FAZNavSubMain', 'FAZImgEvent']),
+ dict(name=['jksrdt'])]
feeds = [(u'Politik', u'http://www.faz.net/s/RubA24ECD630CAE40E483841DB7D16F4211/Tpl~Epartner~SRss_.xml'),
From d2036d1c3043caaf56e1cc968feace1ee3de9885 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Thu, 26 May 2011 13:31:04 -0600
Subject: [PATCH 30/30] Various German news sources by schuster
---
recipes/aachener_nachrichten.recipe | 42 +++++++++++++++++++++
recipes/frankfurter_rundschau.recipe | 35 ++++++++++++++++++
recipes/rheinische_post.recipe | 55 ++++++++++++++++++++++++++++
resources/template-functions.json | 15 +++++---
4 files changed, 141 insertions(+), 6 deletions(-)
create mode 100644 recipes/aachener_nachrichten.recipe
create mode 100644 recipes/frankfurter_rundschau.recipe
create mode 100644 recipes/rheinische_post.recipe
diff --git a/recipes/aachener_nachrichten.recipe b/recipes/aachener_nachrichten.recipe
new file mode 100644
index 0000000000..a2294fc472
--- /dev/null
+++ b/recipes/aachener_nachrichten.recipe
@@ -0,0 +1,42 @@
+from calibre.web.feeds.recipes import BasicNewsRecipe
+class AdvancedUserRecipe(BasicNewsRecipe):
+
+ title = u'Aachener Nachrichten'
+ __author__ = 'schuster'
+ oldest_article = 1
+ max_articles_per_feed = 100
+ use_embedded_content = False
+ language = 'de'
+ remove_javascript = True
+ cover_url = 'http://www.an-online.de/einwaage/images/an_logo.png'
+ masthead_url = 'http://www.an-online.de/einwaage/images/an_logo.png'
+ extra_css = '''
+ .fliesstext_detail:{margin-bottom:10%;}
+ .headline_1:{margin-bottom:25%;}
+ b{font-family:Arial,Helvetica,sans-serif; font-weight:200;font-size:large;}
+ a{font-family:Arial,Helvetica,sans-serif; font-weight:400;font-size:large;}
+ ll{font-family:Arial,Helvetica,sans-serif; font-weight:100;font-size:large;}
+ h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
+ img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
+ dd{font-family:Arial,Helvetica,sans-serif;font-size:large;}
+ body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
+ '''
+
+
+
+ keep_only_tags = [
+ dict(name='span', attrs={'class':['fliesstext_detail', 'headline_1', 'autor_detail']}),
+ dict(id=['header-logo'])
+ ]
+
+ feeds = [(u'Euregio', u'http://www.an-online.de/an/rss/Euregio.xml'),
+ (u'Aachen', u'http://www.an-online.de/an/rss/Aachen.xml'),
+ (u'Nordkreis', u'http://www.an-online.de/an/rss/Nordkreis.xml'),
+ (u'Düren', u'http://www.an-online.de/an/rss/Dueren.xml'),
+ (u'Eiffel', u'http://www.an-online.de/an/rss/Eifel.xml'),
+ (u'Eschweiler', u'http://www.an-online.de/an/rss/Eschweiler.xml'),
+ (u'Geilenkirchen', u'http://www.an-online.de/an/rss/Geilenkirchen.xml'),
+ (u'Heinsberg', u'http://www.an-online.de/an/rss/Heinsberg.xml'),
+ (u'Jülich', u'http://www.an-online.de/an/rss/Juelich.xml'),
+ (u'Stolberg', u'http://www.an-online.de/an/rss/Stolberg.xml'),
+ (u'Ratgebenr', u'http://www.an-online.de/an/rss/Ratgeber.xml')]
diff --git a/recipes/frankfurter_rundschau.recipe b/recipes/frankfurter_rundschau.recipe
new file mode 100644
index 0000000000..3c3bb32ca3
--- /dev/null
+++ b/recipes/frankfurter_rundschau.recipe
@@ -0,0 +1,35 @@
+from calibre.web.feeds.recipes import BasicNewsRecipe
+class AdvancedUserRecipe(BasicNewsRecipe):
+
+ title = u'Frankfurter Rundschau'
+ __author__ = 'schuster'
+ oldest_article = 1
+ max_articles_per_feed = 100
+ no_stylesheets = True
+ use_embedded_content = False
+ language = 'de'
+ remove_javascript = True
+ cover_url = 'http://www.fr-online.de/image/view/-/1474018/data/823538/-/logo.png'
+ extra_css = '''
+ h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
+ h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
+ img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
+ p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
+ body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
+ '''
+
+ feeds = [(u'Startseite', u'http://www.fr-online.de/home/-/1472778/1472778/-/view/asFeed/-/index.xml'),
+ (u'Politik', u'http://www.fr-online.de/politik/-/1472596/1472596/-/view/asFeed/-/index.xml'),
+ (u'Meinungen', u'http://www.fr-online.de/politik/meinung/-/1472602/1472602/-/view/asFeed/-/index.xml'),
+ (u'Wirtschaft', u'http://www.fr-online.de/wirtschaft/-/1472780/1472780/-/view/asFeed/-/index.xml'),
+ (u'Sport', u'http://www.fr-online.de/sport/-/1472784/1472784/-/view/asFeed/-/index.xml'),
+ (u'Kultur', u'http://www.fr-online.de/kultur/-/1472786/1472786/-/view/asFeed/-/index.xml'),
+ (u'Panorama', u'http://www.fr-online.de/panorama/-/1472782/1472782/-/view/asFeed/-/index.xml'),
+ (u'Digital', u'http://www.fr-online.de/digital/-/1472406/1472406/-/view/asFeed/-/index.xml'),
+ (u'Wissenschaft', u'http://www.fr-online.de/wissenschaft/-/1472788/1472788/-/view/asFeed/-/index.xml')
+]
+
+
+ def print_version(self, url):
+ return url.replace('index.html', 'view/printVersion/-/index.html')
+
diff --git a/recipes/rheinische_post.recipe b/recipes/rheinische_post.recipe
new file mode 100644
index 0000000000..1d3efc710d
--- /dev/null
+++ b/recipes/rheinische_post.recipe
@@ -0,0 +1,55 @@
+from calibre.web.feeds.recipes import BasicNewsRecipe
+class AdvancedUserRecipe(BasicNewsRecipe):
+
+ title = u'RP-online'
+ __author__ = 'schuster'
+ oldest_article = 2
+ max_articles_per_feed = 100
+ no_stylesheets = True
+ use_embedded_content = False
+ language = 'de'
+ remove_javascript = True
+ masthead_url = 'http://www.die-zeitungen.de/uploads/pics/LOGO_RP_ONLINE_01.jpg'
+ cover_url = 'http://www.manroland.com/com/pressinfo_images/com/RheinischePost_Logo_300dpi.jpg'
+ extra_css = '''
+ h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
+ h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
+ img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
+ p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
+ body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
+ '''
+ remove_tags_before = dict(id='article_content')
+ remove_tags_after = dict(id='article_content')
+ remove_tags = [dict(attrs={'class':['goodies', 'left', 'right', 'clear-all', 'teaser anzeigenwerbung', 'lesermeinung', 'goodiebox', 'goodiebox 1', 'goodiebox 2', 'goodiebox 3', 'boxframe', 'link']}),
+ dict(id=['click_Fotos_link']),
+ dict(name=['script', 'noscript', 'style', '_top', 'click_Fotos_link'])]
+
+ feeds = [ (u'Top-News', u'http://www.ngz-online.de/app/feed/rss/topnews'),
+ (u'Politik', u'http://www.ngz-online.de/app/feed/rss/politik'),
+ (u'Wirtschaft', u'http://www.ngz-online.de/app/feed/rss/wirtschaft'),
+ (u'Panorama', u'http://www.ngz-online.de/app/feed/rss/panorama'),
+ (u'Sport', u'http://www.ngz-online.de/app/feed/rss/sport'),
+ (u'Tour de France', u'http://www.ngz-online.de/app/feed/rss/tourdefrance'),
+ (u'Fußball', u'http://www.ngz-online.de/app/feed/rss/fussball'),
+ (u'Fußball BuLi', u'http://www.ngz-online.de/app/feed/rss/bundesliga'),
+ (u'Formel 1', u'http://www.ngz-online.de/app/feed/rss/formel1'),
+ (u'US-Sport', u'http://www.ngz-online.de/app/feed/rss/us-sports'),
+ (u'Boxen', u'http://www.ngz-online.de/app/feed/rss/boxen'),
+ (u'Eishockey', u'http://www.ngz-online.de/app/feed/rss/eishockey'),
+ (u'Basketball', u'http://www.ngz-online.de/app/feed/rss/basketball'),
+ (u'Handball', u'http://www.ngz-online.de/app/feed/rss/handball'),
+ (u'Motorsport', u'http://www.ngz-online.de/app/feed/rss/motorsport'),
+ (u'Tennis', u'http://www.ngz-online.de/app/feed/rss/tennis'),
+ (u'Radsport', u'http://www.ngz-online.de/app/feed/rss/radsport'),
+ (u'Kultur', u'http://www.ngz-online.de/app/feed/rss/kultur'),
+ (u'Gesellschaft', u'http://www.ngz-online.de/app/feed/rss/gesellschaft'),
+ (u'Wissenschaft', u'http://www.ngz-online.de/app/feed/rss/wissen'),
+ (u'Gesundheit', u'http://www.ngz-online.de/app/feed/rss/gesundheit'),
+ (u'Digitale Welt', u'http://www.ngz-online.de/app/feed/rss/digitale'),
+ (u'Auto & Mobil', u'http://www.ngz-online.de/app/feed/rss/auto'),
+ (u'Reise & Welt', u'http://www.ngz-online.de/app/feed/rss/reise'),
+ (u'Beruf & Karriere', u'http://www.ngz-online.de/app/feed/rss/beruf'),
+ (u'Herzrasen', u'http://www.ngz-online.de/app/feed/rss/herzrasen'),
+ (u'About a Boy', u'http://www.ngz-online.de/app/feed/rss/about_a_boy'),
+
+]
diff --git a/resources/template-functions.json b/resources/template-functions.json
index 7656db4021..b0a2225dd4 100644
--- a/resources/template-functions.json
+++ b/resources/template-functions.json
@@ -1,19 +1,21 @@
{
+ "and": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if not args[i]:\n return ''\n i += 1\n return '1'\n",
"contains": "def evaluate(self, formatter, kwargs, mi, locals,\n val, test, value_if_present, value_if_not):\n if re.search(test, val):\n return value_if_present\n else:\n return value_if_not\n",
"divide": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x / y)\n",
"uppercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.upper()\n",
"strcat": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n res = ''\n for i in range(0, len(args)):\n res += args[i]\n return res\n",
"in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n for v in l:\n if re.search(pat, v):\n return fv\n return nfv\n",
- "substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n",
+ "multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n",
"ifempty": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):\n if val:\n return val\n else:\n return value_if_empty\n",
"booksize": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.book_size is not None:\n try:\n return str(mi.book_size)\n except:\n pass\n return ''\n",
"select": "def evaluate(self, formatter, kwargs, mi, locals, val, key):\n if not val:\n return ''\n vals = [v.strip() for v in val.split(',')]\n for v in vals:\n if v.startswith(key+':'):\n return v[len(key)+1:]\n return ''\n",
+ "strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n",
"first_non_empty": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return args[i]\n i += 1\n return ''\n",
- "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n",
+ "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val)\n",
"subtract": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x - y)\n",
"list_item": "def evaluate(self, formatter, kwargs, mi, locals, val, index, sep):\n if not val:\n return ''\n index = int(index)\n val = val.split(sep)\n try:\n return val[index]\n except:\n return ''\n",
"shorten": "def evaluate(self, formatter, kwargs, mi, locals,\n val, leading, center_string, trailing):\n l = max(0, int(leading))\n t = max(0, int(trailing))\n if len(val) > l + len(center_string) + t:\n return val[0:l] + center_string + ('' if t == 0 else val[-t:])\n else:\n return val\n",
- "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val)\n",
+ "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n",
"add": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x + y)\n",
"lookup": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if len(args) == 2: # here for backwards compatibility\n if val:\n return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)\n else:\n return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)\n if (len(args) % 2) != 1:\n raise ValueError(_('lookup requires either 2 or an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)\n if re.search(args[i], val):\n return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)\n i += 2\n",
"template": "def evaluate(self, formatter, kwargs, mi, locals, template):\n template = template.replace('[[', '{').replace(']]', '}')\n return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)\n",
@@ -23,14 +25,15 @@
"sublist": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n val = val.split(sep)\n try:\n if ei == 0:\n return sep.join(val[si:])\n else:\n return sep.join(val[si:ei])\n except:\n return ''\n",
"test": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set):\n if val:\n return value_if_set\n else:\n return value_not_set\n",
"eval": "def evaluate(self, formatter, kwargs, mi, locals, template):\n from formatter import eval_formatter\n template = template.replace('[[', '{').replace(']]', '}')\n return eval_formatter.safe_format(template, locals, 'EVAL', None)\n",
- "multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n",
+ "not": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n",
"format_date": "def evaluate(self, formatter, kwargs, mi, locals, val, format_string):\n if not val:\n return ''\n try:\n dt = parse_date(val)\n s = format_date(dt, format_string)\n except:\n s = 'BAD DATE'\n return s\n",
"capitalize": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return capitalize(val)\n",
"count": "def evaluate(self, formatter, kwargs, mi, locals, val, sep):\n return unicode(len(val.split(sep)))\n",
"lowercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.lower()\n",
- "strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n",
- "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n",
+ "substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n",
"assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n",
+ "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n",
+ "or": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n",
"raw_field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return unicode(getattr(mi, name, None))\n",
"cmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n x = float(x if x else 0)\n y = float(y if y else 0)\n if x < y:\n return lt\n if x == y:\n return eq\n return gt\n"
}
\ No newline at end of file