From a5cadce60355a8afc49c0bd37375fff78db84d3e Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 27 Nov 2010 11:30:52 +0000 Subject: [PATCH 01/11] Fix #7459: Bug/error when copying and deleting from one library to another. Problem arose because field_metadata modified a class_level (static) attribute. --- src/calibre/library/field_metadata.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index dbc871026e..afc63cb692 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -3,6 +3,7 @@ Created on 25 May 2010 @author: charles ''' +import copy from calibre.utils.ordered_dict import OrderedDict from calibre.utils.config import tweaks @@ -86,7 +87,7 @@ class FieldMetadata(dict): # Builtin metadata {{{ - _field_metadata = [ + _field_metadata_prototype = [ ('authors', {'table':'authors', 'column':'name', 'link_column':'author', @@ -322,6 +323,7 @@ class FieldMetadata(dict): ] def __init__(self): + self._field_metadata = copy.deepcopy(self._field_metadata_prototype) self._tb_cats = OrderedDict() self._search_term_map = {} self.custom_label_to_key_map = {} From 9e756e0a9630bb5da8dec82fbc7a18cd6ce06f21 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 27 Nov 2010 12:09:12 +0000 Subject: [PATCH 02/11] Fix #7548: Edit bulk m'data won't edit all if one instance is already specified. The None value was ambiguous, meaning both None and No Change. Added a 'Do not change' option to eliminate the ambiguity. --- src/calibre/gui2/custom_column_widgets.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 3103d7c459..053dd7a743 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -437,7 +437,7 @@ class BulkBool(BulkBase, Bool): if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: val = False if value is not None and value != val: - return None + return 'nochange' value = val return value @@ -445,19 +445,23 @@ class BulkBool(BulkBase, Bool): self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), QComboBox(parent)] w = self.widgets[1] - items = [_('Yes'), _('No'), _('Undefined')] - icons = [I('ok.png'), I('list_remove.png'), I('blank.png')] + items = [_('Yes'), _('No'), _('Undefined'), _('Do not change')] + icons = [I('ok.png'), I('list_remove.png'), I('blank.png'), I('blank.png')] for icon, text in zip(icons, items): w.addItem(QIcon(icon), text) + def getter(self): + val = self.widgets[1].currentIndex() + return {3: 'nochange', 2: None, 1: False, 0: True}[val] + def setter(self, val): - val = {None: 2, False: 1, True: 0}[val] + val = {'nochange': 3, None: 2, False: 1, True: 0}[val] self.widgets[1].setCurrentIndex(val) def commit(self, book_ids, notify=False): val = self.gui_val val = self.normalize_ui_val(val) - if val != self.initial_val: + if val != self.initial_val and val != 'nochange': if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: val = False self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) From be987eac406099888b356eca8ce9a5f9d3f76d74 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 27 Nov 2010 14:03:05 +0000 Subject: [PATCH 03/11] Enhancement #7559: tests on counts of items in multiple fields. --- src/calibre/library/caches.py | 39 ++++++++++++++++++++++++----------- src/calibre/manual/gui.rst | 8 +++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index 075fbe664a..6148019906 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -403,7 +403,7 @@ class ResultCache(SearchQueryParser): # {{{ '<=':[2, lambda r, q: r <= q] } - def get_numeric_matches(self, location, query): + def get_numeric_matches(self, location, query, val_func = None): matches = set([]) if len(query) == 0: return matches @@ -419,7 +419,10 @@ class ResultCache(SearchQueryParser): # {{{ if relop is None: (p, relop) = self.numeric_search_relops['='] - loc = self.field_metadata[location]['rec_index'] + if val_func is None: + loc = self.field_metadata[location]['rec_index'] + val_func = lambda item, loc=loc: item[loc] + dt = self.field_metadata[location]['datatype'] if dt == 'int': cast = (lambda x: int (x)) @@ -430,6 +433,9 @@ class ResultCache(SearchQueryParser): # {{{ elif dt == 'float': cast = lambda x : float (x) adjust = lambda x: x + else: # count operation + cast = (lambda x: int (x)) + adjust = lambda x: x if len(query) > 1: mult = query[-1:].lower() @@ -446,10 +452,11 @@ class ResultCache(SearchQueryParser): # {{{ for item in self._data: if item is None: continue - if not item[loc]: + v = val_func(item) + if not v: i = 0 else: - i = adjust(item[loc]) + i = adjust(v) if relop(i, q): matches.add(item[0]) return matches @@ -467,15 +474,23 @@ class ResultCache(SearchQueryParser): # {{{ return matches raise ParseException(query, len(query), 'Recursive query group detected', self) - # take care of dates special case - if location in self.field_metadata and \ - self.field_metadata[location]['datatype'] == 'datetime': - return self.get_dates_matches(location, query.lower()) + if location in self.field_metadata: + fm = self.field_metadata[location] + # take care of dates special case + if fm['datatype'] == 'datetime': + return self.get_dates_matches(location, query.lower()) - # take care of numbers special case - if location in self.field_metadata and \ - self.field_metadata[location]['datatype'] in ('rating', 'int', 'float'): - return self.get_numeric_matches(location, query.lower()) + # take care of numbers special case + if fm['datatype'] in ('rating', 'int', 'float'): + return self.get_numeric_matches(location, query.lower()) + + # take care of the 'count' operator for is_multiples + if fm['is_multiple'] and \ + len(query) > 1 and query.startswith('#') and \ + query[1:1] in '=<>!': + vf = lambda item, loc=fm['rec_index'], ms=fm['is_multiple']:\ + len(item[loc].split(ms)) if item[loc] is not None else 0 + return self.get_numeric_matches(location, query[1:], val_func=vf) # everything else, or 'all' matches matchkind = CONTAINS_MATCH diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst index c81688ba8c..a91124b214 100644 --- a/src/calibre/manual/gui.rst +++ b/src/calibre/manual/gui.rst @@ -274,6 +274,14 @@ Searching for ``no`` or ``unchecked`` will find all books with ``No`` in the col :guilabel:`Advanced Search Dialog` +You can test for the number of items in multiple-value columns, such as tags, formats, authors, and tags-like custom columns. This is done using a syntax very similar to numeric tests (discussed above), except that the relational operator begins with a ``#`` character. For example:: + + tags:#>3 will give you books with more than three tags + tags:#!=3 will give you books that do not have three tags + authors:#=1 will give you books with exactly one author + #cust:#<5 will give you books with less than five items in custom column #cust + formats:#>1 will give you books with more than one format + Saving searches ----------------- From 9b80244bdb94d20a6c04e0bb810b2b93bc39ab09 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 27 Nov 2010 15:19:15 +0000 Subject: [PATCH 04/11] field_metadata changes to account for the new has_cover DB column. --- src/calibre/library/database2.py | 2 -- src/calibre/library/field_metadata.py | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index ffb97e481f..47c575386b 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -268,8 +268,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): base, prefer_custom=True) - self.field_metadata.set_field_record_index('cover', - self.FIELD_MAP['cover'], prefer_custom=False) self.FIELD_MAP['ondevice'] = base+1 self.field_metadata.set_field_record_index('ondevice', base+1, prefer_custom=False) self.FIELD_MAP['all_metadata'] = base+2 diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index afc63cb692..d10dc5da71 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -162,6 +162,15 @@ class FieldMetadata(dict): 'search_terms':['tags', 'tag'], 'is_custom':False, 'is_category':True}), + ('all_metadata',{'table':None, + 'column':None, + 'datatype':None, + 'is_multiple':None, + 'kind':'field', + 'name':None, + 'search_terms':[], + 'is_custom':False, + 'is_category':False}), ('author_sort',{'table':None, 'column':None, 'datatype':'text', @@ -181,7 +190,7 @@ class FieldMetadata(dict): 'is_custom':False, 'is_category':False}), ('cover', {'table':None, 'column':None, - 'datatype':None, + 'datatype':'int', 'is_multiple':None, 'kind':'field', 'name':None, @@ -224,15 +233,6 @@ class FieldMetadata(dict): 'search_terms':[], 'is_custom':False, 'is_category':False}), - ('all_metadata',{'table':None, - 'column':None, - 'datatype':None, - 'is_multiple':None, - 'kind':'field', - 'name':None, - 'search_terms':[], - 'is_custom':False, - 'is_category':False}), ('ondevice', {'table':None, 'column':None, 'datatype':'text', From 19f12f8a5d59f07cc450011f4914460671db2938 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 Nov 2010 15:17:29 -0700 Subject: [PATCH 05/11] ... --- src/calibre/gui2/book_details.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 28ca06b352..4ffc8da650 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -214,9 +214,18 @@ class BookInfo(QWebView): def _show_data(self, rows, comments): + + def color_to_string(col): + ans = '#000000' + if col.isValid(): + col = col.toRgb() + if col.isValid(): + ans = unicode(col.name()) + return ans + f = QFontInfo(QApplication.font(self.parent())).pixelSize() - c = unicode(QApplication.palette().color(QPalette.Normal, - QPalette.WindowText).name()) + c = color_to_string(QApplication.palette().color(QPalette.Normal, + QPalette.WindowText)) templ = u'''\ From 90462414850f558638db5e644271df043c5b9b35 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 Nov 2010 15:40:59 -0700 Subject: [PATCH 06/11] Make InterfaceAction plugins loadable from zip files --- src/calibre/customize/__init__.py | 9 +++++++++ src/calibre/gui2/ui.py | 4 +--- src/calibre/manual/plugins.rst | 9 +++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 523fc51d92..251f527bb6 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -370,6 +370,15 @@ class InterfaceActionBase(Plugin): # {{{ can_be_disabled = False actual_plugin = None + + def load_actual_plugin(self, gui): + ''' + This method should must the actual interface action plugin object. + ''' + mod, cls = self.actual_plugin.split(':') + return getattr(__import__(mod, fromlist=['1'], level=0), cls)(gui, + self.site_customization) + # }}} class PreferencesPlugin(Plugin): # {{{ diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index bd0743e819..00bba2b491 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -102,9 +102,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.device_connected = None acmap = OrderedDict() for action in interface_actions(): - mod, cls = action.actual_plugin.split(':') - ac = getattr(__import__(mod, fromlist=['1'], level=0), cls)(self, - action.site_customization) + ac = action.load_actual_plugin(self) if ac.name in acmap: if ac.priority >= acmap[ac.name].priority: acmap[ac.name] = ac diff --git a/src/calibre/manual/plugins.rst b/src/calibre/manual/plugins.rst index eb955aebee..0a62218fb9 100644 --- a/src/calibre/manual/plugins.rst +++ b/src/calibre/manual/plugins.rst @@ -161,11 +161,20 @@ The base class for such devices is :class:`calibre.devices.usbms.driver.USBMS`. User Interface Actions -------------------------- +If you are adding your own plugin in a zip file, you should subclass both InterfaceActionBase and InterfaceAction. The :meth:`load_actual_plugin` method of you InterfaceActionBase subclass must return an instantiated object of your InterfaceBase subclass. + + .. autoclass:: calibre.gui2.actions.InterfaceAction :show-inheritance: :members: :member-order: bysource +.. autoclass:: calibre.customize.InterfaceActionBase + :show-inheritance: + :members: + :member-order: bysource + + Preferences Plugins -------------------------- From b3c6bb4cb1e938176150d12429ab0c55748baab0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 Nov 2010 15:45:15 -0700 Subject: [PATCH 07/11] ... --- src/calibre/customize/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 251f527bb6..b4e9b6c448 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -373,7 +373,7 @@ class InterfaceActionBase(Plugin): # {{{ def load_actual_plugin(self, gui): ''' - This method should must the actual interface action plugin object. + This method must return the actual interface action plugin object. ''' mod, cls = self.actual_plugin.split(':') return getattr(__import__(mod, fromlist=['1'], level=0), cls)(gui, From 3ac65b82c08c63f2ec799540cc63f0f3bf2b3411 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 Nov 2010 17:45:48 -0700 Subject: [PATCH 08/11] A Hello World GUI plugin --- src/calibre/manual/customize.rst | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst index e0f799f572..5272a88f53 100644 --- a/src/calibre/manual/customize.rst +++ b/src/calibre/manual/customize.rst @@ -98,6 +98,44 @@ Every time you use calibre to convert a book, the plugin's :meth:`run` method wi converted book will have its publisher set to "Hello World". For more information about |app|'s plugin system, read on... + +A Hello World GUI plugin +--------------------------- + +Here's a simple Hello World plugin for the |app| GUI. It will cause a box to popup with the message "Hellooo World!" when you press Ctrl+Shift+H + +.. code-block:: python + + from calibre.customize import InterfaceActionBase + + class HelloWorldBase(InterfaceActionBase): + + name = 'Hello World GUI' + author = 'The little green man' + + def load_actual_plugin(self, gui): + from calibre.gui2.actions import InterfaceAction + + class HelloWorld(InterfaceAction): + name = 'Hello World GUI' + action_spec = ('Hello World!', 'add_book.png', None, + _('Ctrl+Shift+H')) + + def genesis(self): + self.qaction.triggered.connect(self.hello_world) + + def hello_world(self, *args): + from calibre.gui2 import info_dialog + info_dialog(self.gui, 'Hello World!', 'Hellooo World!', + show=True) + + return HelloWorld(gui, None) + +You can also have it show up in the toolbars/context menu by going to Preferences->Toolbars and adding this plugin to the locations you want it to be in. + +While this plugin is utterly useless, note that all calibre GUI actions like adding/saving/removing/viewing/etc. are implemented as plugins, so there is no limit to what you can acheive. The key thing to remember is that the plugin has access to the full |app| GUI via ``self.gui``. + + The Plugin base class ------------------------ From fa4986e6c897f8c32208308aa488f455ee3337de Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 Nov 2010 19:05:13 -0700 Subject: [PATCH 09/11] Improve Revista Muy Intersante --- resources/recipes/revista_muy.recipe | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/resources/recipes/revista_muy.recipe b/resources/recipes/revista_muy.recipe index b101fe97ce..c4516f718d 100644 --- a/resources/recipes/revista_muy.recipe +++ b/resources/recipes/revista_muy.recipe @@ -13,6 +13,8 @@ class RevistaMuyInteresante(BasicNewsRecipe): no_stylesheets = True remove_javascript = True + conversion_options = {'linearize_tables': True} + extra_css = ' .txt_articulo{ font-family: sans-serif; font-size: medium; text-align: justify } .contentheading{font-family: serif; font-size: large; font-weight: bold; color: #000000; text-align: center}' @@ -39,11 +41,12 @@ class RevistaMuyInteresante(BasicNewsRecipe): keep_only_tags = [dict(name='div', attrs={'class':['article']}),dict(name='td', attrs={'class':['txt_articulo']})] remove_tags = [ - dict(name=['object','link','script','ul']) + dict(name=['object','link','script','ul','iframe','ins']) ,dict(name='div', attrs={'id':['comment']}) ,dict(name='td', attrs={'class':['buttonheading']}) - ,dict(name='div', attrs={'class':['tags_articles']}) + ,dict(name='div', attrs={'class':['tags_articles','bajo_title']}) ,dict(name='table', attrs={'class':['pagenav']}) + ,dict(name='form', attrs={'class':['voteform']}) ] remove_tags_after = dict(name='div', attrs={'class':'tags_articles'}) @@ -115,3 +118,5 @@ class RevistaMuyInteresante(BasicNewsRecipe): if link_item: cover_url = "http://www.muyinteresante.es"+link_item['src'] return cover_url + + From 3a7da561cc69594b4a61306241e1ca330a17fbc6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 Nov 2010 19:31:12 -0700 Subject: [PATCH 10/11] Fix regression taht broke RTF conversion on some linux systems --- src/calibre/ebooks/rtf/input.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/rtf/input.py b/src/calibre/ebooks/rtf/input.py index 75c839eb83..57903a6711 100644 --- a/src/calibre/ebooks/rtf/input.py +++ b/src/calibre/ebooks/rtf/input.py @@ -292,7 +292,8 @@ class RTFInput(InputFormatPlugin): # Replace newlines inserted by the 'empty_paragraphs' option in rtf2xml with html blank lines if not getattr(self.options, 'remove_paragraph_spacing', False): res = re.sub('\s*', '', res) - res = re.sub('(?<=\n)\n{2}', u'

\u00a0

\n', res) + res = re.sub('(?<=\n)\n{2}', + u'

\u00a0

\n'.encode('utf-8'), res) if self.options.preprocess_html: preprocessor = PreProcessor(self.options, log=getattr(self, 'log', None)) res = preprocessor(res) From 3cea3ec006db7d81449a7ea3cfec7bf7b07d8253 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 Nov 2010 19:33:05 -0700 Subject: [PATCH 11/11] ... --- src/calibre/manual/customize.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/manual/customize.rst b/src/calibre/manual/customize.rst index 5272a88f53..0d6f75b9cf 100644 --- a/src/calibre/manual/customize.rst +++ b/src/calibre/manual/customize.rst @@ -129,11 +129,11 @@ Here's a simple Hello World plugin for the |app| GUI. It will cause a box to pop info_dialog(self.gui, 'Hello World!', 'Hellooo World!', show=True) - return HelloWorld(gui, None) + return HelloWorld(gui, self.site_customization) You can also have it show up in the toolbars/context menu by going to Preferences->Toolbars and adding this plugin to the locations you want it to be in. -While this plugin is utterly useless, note that all calibre GUI actions like adding/saving/removing/viewing/etc. are implemented as plugins, so there is no limit to what you can acheive. The key thing to remember is that the plugin has access to the full |app| GUI via ``self.gui``. +While this plugin is utterly useless, note that all calibre GUI actions like adding/saving/removing/viewing/etc. are implemented as plugins, so there is no limit to what you can achieve. The key thing to remember is that the plugin has access to the full |app| GUI via ``self.gui``. The Plugin base class