From 7fe53cd96b0a13d3d8cadd7429aa32db81698005 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 31 May 2011 17:46:12 -0600 Subject: [PATCH 01/17] Fix #790945 (Updated recipe for BBC News (fast)) --- recipes/bbc_fast.recipe | 78 +++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/recipes/bbc_fast.recipe b/recipes/bbc_fast.recipe index 93ee11ce32..9bcbfb5f70 100644 --- a/recipes/bbc_fast.recipe +++ b/recipes/bbc_fast.recipe @@ -1,27 +1,30 @@ __license__ = 'GPL v3' -__copyright__ = '2010, Darko Miletic ' +__copyright__ = '2010 - 2011, Darko Miletic ' ''' news.bbc.co.uk ''' -import re from calibre.web.feeds.recipes import BasicNewsRecipe class BBC(BasicNewsRecipe): title = 'BBC News (fast)' __author__ = 'Darko Miletic, Starson17' - description = 'News from UK. A much faster version that does not download pictures' + description = 'Visit BBC News for up-to-the-minute news, breaking news, video, audio and feature stories. BBC News provides trusted World and UK news as well as local and regional perspectives. Also entertainment, business, science, technology and health news.' oldest_article = 2 max_articles_per_feed = 100 no_stylesheets = True - #delay = 1 use_embedded_content = False encoding = 'utf8' publisher = 'BBC' category = 'news, UK, world' language = 'en_GB' publication_type = 'newsportal' - extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} ' - preprocess_regexps = [(re.compile(r'', re.DOTALL), lambda m: '')] + masthead_url = 'http://news.bbcimg.co.uk/img/1_0_1/cream/hi/news/news-blocks.gif' + extra_css = """ + body{ font-family: Verdana,Helvetica,Arial,sans-serif } + .introduction{font-weight: bold} + .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} + .story-feature h2{text-align: center; text-transform: uppercase} + """ conversion_options = { 'comments' : description ,'tags' : category @@ -31,31 +34,54 @@ class BBC(BasicNewsRecipe): } keep_only_tags = [ - dict(name='div', attrs={'class':['layout-block-a layout-block']}) - ,dict(attrs={'class':['story-body','storybody']}) + dict(name='div', attrs={'class':['layout-block-a layout-block']}) + ,dict(attrs={'class':['story-body','storybody']}) + ,dict(attrs={'id':['meta-information','story-body']}) ] remove_tags = [ - dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \ - 'story-feature wide ', 'story-feature narrow']}) - , dict(name=['img']) - ] + dict(name='div', attrs={'class':['story-feature related narrow', \ + 'share-help', 'embedded-hyper', \ + 'story-feature wide ', \ + 'story-feature narrow', \ + 'hidden','story-actions', \ + 'embedded-hyper']}) + ,dict(name=['img','meta','link','object','embed','iframe','base']) + ,dict(attrs={'class':['hidden','videoInStoryC']}) + ,dict(attrs={'id':['bbccom_sponsor_section','toggle-controls', \ + 'toggle-images','toggle-title']}) + ] - remove_attributes = ['width','height'] + remove_attributes = ['width','height','xmlns:og','lang','clear'] feeds = [ - ('News Front Page', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml'), - ('Science/Nature', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/science/nature/rss.xml'), - ('Technology', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/technology/rss.xml'), - ('Entertainment', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/entertainment/rss.xml'), - ('Magazine', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/uk_news/magazine/rss.xml'), - ('Business', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/business/rss.xml'), - ('Health', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/health/rss.xml'), - ('Americas', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/americas/rss.xml'), - ('Europe', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/europe/rss.xml'), - ('South Asia', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/south_asia/rss.xml'), - ('UK', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/uk_news/rss.xml'), - ('Asia-Pacific', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/asia-pacific/rss.xml'), - ('Africa', 'http://newsrss.bbc.co.uk/rss/newsonline_world_edition/africa/rss.xml'), + ('Top Stories' , 'http://feeds.bbci.co.uk/news/rss.xml' ), + ('Science/Environment', 'http://feeds.bbci.co.uk/news/science_and_environment/rss.xml'), + ('Technology' , 'http://feeds.bbci.co.uk/news/technology/rss.xml' ), + ('Entertainment/Arts' , 'http://feeds.bbci.co.uk/news/entertainment_and_arts/rss.xml' ), + ('Magazine' , 'http://feeds.bbci.co.uk/news/magazine/rss.xml' ), + ('Business' , 'http://feeds.bbci.co.uk/news/business/rss.xml' ), + ('Politics' , 'http://feeds.bbci.co.uk/news/politics/rss.xml' ), + ('Health' , 'http://feeds.bbci.co.uk/news/health/rss.xml' ), + ('US&Canada' , 'http://feeds.bbci.co.uk/news/world/us_and_canada/rss.xml' ), + ('Latin America' , 'http://feeds.bbci.co.uk/news/world/latin_america/rss.xml' ), + ('Europe' , 'http://feeds.bbci.co.uk/news/world/europe/rss.xml' ), + ('South Asia' , 'http://feeds.bbci.co.uk/news/world/south_asia/rss.xml' ), + ('England' , 'http://feeds.bbci.co.uk/news/england/rss.xml' ), + ('Asia-Pacific' , 'http://feeds.bbci.co.uk/news/world/asia_pacific/rss.xml' ), + ('Africa' , 'http://feeds.bbci.co.uk/news/world/africa/rss.xml' ) ] + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + for item in soup.findAll('left'): + item.name='span' + for item in soup.findAll('a'): + if item.string is not None: + str = item.string + item.replaceWith(str) + else: + str = self.tag_to_string(item) + item.replaceWith(str) + return soup From 202c9b9d144052cd284ac68886f262d3eda560d8 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 1 Jun 2011 05:37:59 +0100 Subject: [PATCH 02/17] template box. --- .../gui2/dialogs/template_line_editor.py | 166 ++++++++++++------ src/calibre/utils/formatter_functions.py | 4 +- 2 files changed, 111 insertions(+), 59 deletions(-) diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py index 6a0b07200e..af70a16d31 100644 --- a/src/calibre/gui2/dialogs/template_line_editor.py +++ b/src/calibre/gui2/dialogs/template_line_editor.py @@ -95,25 +95,27 @@ class TemplateLineEditor(QLineEdit): class TagWizard(QDialog): - text_template = " strcmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')" - text_empty_template = " test(field('{f}'), '{fv}', '{tv}')" - text_re_template = " contains(field('{f}'), '{v}', '{tv}', '{fv}')" + text_template = (" strcmp(field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True) + text_empty_template = (" test(field('{f}'), '{fv}', '{tv}')", False) + text_re_template = (" contains(field('{f}'), '{v}', '{tv}', '{fv}')", False) templates = { - 'text.mult' : " str_in_list(field('{f}'), '{mult}', '{v}', '{tv}', '{fv}')", - 'text.mult.re' : " in_list(field('{f}'), '{mult}', '^{v}$', '{tv}', '{fv}')", - 'text.mult.empty' : " test(field('{f}'), '{fv}', '{tv}')", + 'text.mult' : (" str_in_list(field('{f}'), '{mult}', '{v}', '{tv}', '{fv}')", False), + 'text.mult.re' : (" in_list(field('{f}'), '{mult}', '^{v}$', '{tv}', '{fv}')", False), + 'text.mult.empty' : (" test(field('{f}'), '{fv}', '{tv}')", False), 'text' : text_template, 'text.re' : text_re_template, 'text.empty' : text_empty_template, - 'rating' : " cmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')", + 'rating' : (" cmp(raw_field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True), 'rating.empty' : text_empty_template, - 'int' : " cmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')", + 'int' : (" cmp(raw_field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True), 'int.empty' : text_empty_template, - 'float' : " cmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')", + 'float' : (" cmp(raw_field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True), 'float.empty' : text_empty_template, - 'bool' : " strcmp(field('{f}'), '{v}', '{fv}', '{tv}', '{fv}')", + 'bool' : (" strcmp(field('{f}'), '{v}', '{ltv}', '{eqv}', '{gtv}')", True), 'bool.empty' : text_empty_template, + 'datetime' : (" strcmp(format_date(raw_field('{f}'), 'yyyyMMdd'), format_date('{v}', 'yyyyMMdd'), '{ltv}', '{eqv}', '{gtv}')", True), + 'datetime.empty' : text_empty_template, 'series' : text_template, 'series.re' : text_re_template, 'series.empty' : text_empty_template, @@ -128,6 +130,22 @@ class TagWizard(QDialog): 'comments.empty' : text_empty_template, } + relationals = ('=', '!=', '<', '>', '<=', '>=') + relational_truth_vals = { + '=': ('', '1', ''), + '!=': ('1', '', '1'), + '<': ('1', '', ''), + '>': ('', '', '1'), + '<=': ('1', '1', ''), + '>=': ('', '1', '1'), + } + + @staticmethod + def uses_this_wizard(txt): + if not txt or txt.startswith('program:\n#tag wizard'): + return True + return False + def __init__(self, parent, db, txt, mi): QDialog.__init__(self, parent) self.setWindowTitle(_('Coloring Wizard')) @@ -141,8 +159,7 @@ class TagWizard(QDialog): m = db.metadata_for_field(k) if k.endswith('_index') or ( m['kind'] == 'field' and m['name'] and - k not in ('ondevice', 'path', 'size', 'sort') and - m['datatype'] not in ('datetime')): + k not in ('ondevice', 'path', 'size', 'sort')): self.columns.append(k) self.completion_values[k]['dt'] = m['datatype'] if m['is_custom']: @@ -203,11 +220,12 @@ class TagWizard(QDialog): h.setAlignment(Qt.AlignCenter) l.addWidget(h, 0, 2, 1, 1) - h = QLabel(_('not')) + h = QLabel(_('op')) h.setToolTip('

' + - _('Check this box to indicate that the value must not match ' - 'to use the color. For example, you can check if a tag does ' - 'not exist by entering that tag and checking this box.') + '

') + _('Use this box to tell what comparison operation to use. Some ' + 'comparisons cannot be used with certain options. For example, ' + 'if regular expressions are used, only equals and not equals ' + 'are valid.') + '

') h.setAlignment(Qt.AlignCenter) l.addWidget(h, 0, 3, 1, 1) @@ -246,7 +264,7 @@ class TagWizard(QDialog): l.addWidget(c, 0, 7, 1, 1) self.andboxes = [] - self.notboxes = [] + self.opboxes = [] self.tagboxes = [] self.colorboxes = [] self.reboxes = [] @@ -284,13 +302,17 @@ class TagWizard(QDialog): w.setText(_('is')) l.addWidget(w, i, 2, 1, 1) - create_widget(QCheckBox, self.notboxes, l, i, 3, None) + w = create_widget(QComboBox, self.opboxes, l, i, 3, None) + w.setMaximumWidth(40) w = create_widget(QCheckBox, self.emptyboxes, l, i, 4, None) w.stateChanged.connect(partial(self.empty_box_changed, line=i-1)) create_widget(MultiCompleteLineEdit, self.tagboxes, l, i, 5, None, align=0) - create_widget(QCheckBox, self.reboxes, l, i, 6, None) + + w = create_widget(QCheckBox, self.reboxes, l, i, 6, None) + w.stateChanged.connect(partial(self.re_box_changed, line=i-1)) + create_widget(QComboBox, self.colorboxes, l, i, 7, self.colors) w = create_widget(QLabel, None, l, maxlines+1, 5, None) @@ -315,20 +337,23 @@ class TagWizard(QDialog): if len(vals) == 2: t, c = vals f = 'tags' - a = n = e = re = False + a = re = e = 0 + op = '=' else: - t,c,f,re,a,n,e = vals + t,c,f,re,a,op,e = vals try: self.colboxes[i].setCurrentIndex(self.colboxes[i].findText(f)) self.colorboxes[i].setCurrentIndex( self.colorboxes[i].findText(c)) self.tagboxes[i].setText(t) self.reboxes[i].setChecked(re == '2') - self.andboxes[i].setChecked(a == '2') - self.notboxes[i].setChecked(n == '2') self.emptyboxes[i].setChecked(e == '2') + self.andboxes[i].setChecked(a == '2') + self.opboxes[i].setCurrentIndex(self.opboxes[i].findText(op)) i += 1 except: + import traceback + traceback.print_exc() pass w = QLabel(_('Preview')) @@ -357,26 +382,6 @@ class TagWizard(QDialog): _('EXCEPTION'), self.mi) self.test_box.setText(t) - def column_changed(self, s, line=None): - k = unicode(s) - if k in self.completion_values: - valbox = self.tagboxes[line] - valbox.update_items_cache(self.completion_values[k]['v']) - if self.completion_values[k]['m']: - valbox.set_separator(', ') - else: - valbox.set_separator(None) - - dt = self.completion_values[k]['dt'] - if dt in ('int', 'float', 'rating', 'bool'): - self.reboxes[line].setChecked(0) - self.reboxes[line].setEnabled(False) - else: - self.reboxes[line].setEnabled(True) - else: - valbox.update_items_cache([]) - valbox.set_separator(None) - def generate_program(self): res = ("program:\n#tag wizard -- do not directly edit\n" " first_non_empty(\n") @@ -384,9 +389,9 @@ class TagWizard(QDialog): was_and = had_line = False line = 0 - for tb, cb, fb, reb, ab, nb, eb in zip( + for tb, cb, fb, reb, ab, ob, eb in zip( self.tagboxes, self.colorboxes, self.colboxes, - self.reboxes, self.andboxes, self.notboxes, self.emptyboxes): + self.reboxes, self.andboxes, self.opboxes, self.emptyboxes): f = unicode(fb.currentText()) if not f: continue @@ -394,14 +399,11 @@ class TagWizard(QDialog): dt = self.completion_values[f]['dt'] c = unicode(cb.currentText()).strip() re = reb.checkState() - a = ab.checkState() - n = nb.checkState() - e = eb.checkState() + a = ab.checkState() + op = unicode(ob.currentText()) + e = eb.checkState() line += 1 - tval = '' if n == 2 else '1' - fval = '1' if n == 2 else '' - if m: tags = [t.strip() for t in unicode(tb.text()).split(m) if t.strip()] if re == 2: @@ -428,8 +430,15 @@ class TagWizard(QDialog): lines[-1] += ',' key = dt + ('.mult' if m else '') + ('.empty' if e else '') + ('.re' if re else '') - template = self.templates[key] - lines.append(template.format(v=tags, f=f, tv=tval, fv=fval, mult=m)) + tval = '1' if op == '=' else '' + fval = '' if op == '=' else '1' + template, is_relational = self.templates[key] + if is_relational: + ltv, eqv, gtv = self.relational_truth_vals[op] + else: + ltv, eqv, gtv = (None, None, None) + lines.append(template.format(v=tags, f=f, tv=tval, fv=fval, mult=m, + ltv=ltv, eqv=eqv, gtv=gtv)) if a == 2: was_and = True @@ -444,9 +453,9 @@ class TagWizard(QDialog): res += ')\n' self.template = res res = '' - for tb, cb, fb, reb, ab, nb, eb in zip( + for tb, cb, fb, reb, ab, ob, eb in zip( self.tagboxes, self.colorboxes, self.colboxes, - self.reboxes, self.andboxes, self.notboxes, self.emptyboxes): + self.reboxes, self.andboxes, self.opboxes, self.emptyboxes): t = unicode(tb.text()).strip() if t.endswith(','): t = t[:-1] @@ -454,15 +463,57 @@ class TagWizard(QDialog): f = unicode(fb.currentText()) re = unicode(reb.checkState()) a = unicode(ab.checkState()) - n = unicode(nb.checkState()) + op = unicode(ob.currentText()) e = unicode(eb.checkState()) if f and (t or e) and (a == '2' or c): res += '#' + t + ':|:' + c + ':|:' + f + ':|:' + re + ':|:' + \ - a + ':|:' + n + ':|:' + e + '\n' + a + ':|:' + op + ':|:' + e + '\n' res += '#else:' + else_txt + '\n' self.template += res return True + def column_changed(self, s, line=None): + k = unicode(s) + valbox = self.tagboxes[line] + if k in self.completion_values: + valbox.update_items_cache(self.completion_values[k]['v']) + if self.completion_values[k]['m']: + valbox.set_separator(', ') + else: + valbox.set_separator(None) + + dt = self.completion_values[k]['dt'] + if dt in ('int', 'float', 'rating', 'bool'): + self.reboxes[line].setChecked(0) + self.reboxes[line].setEnabled(False) + else: + self.reboxes[line].setEnabled(True) + self.fill_in_opbox(line) + else: + valbox.update_items_cache([]) + valbox.set_separator(None) + + def fill_in_opbox(self, line): + opbox = self.opboxes[line] + opbox.clear() + k = unicode(self.colboxes[line].currentText()) + if not k: + return + if k in self.completion_values: + rebox = self.reboxes[line] + ebox = self.emptyboxes[line] + idx = opbox.currentIndex() + if self.completion_values[k]['m'] or \ + rebox.checkState() == 2 or ebox.checkState() == 2: + opbox.addItems(self.relationals[0:2]) + idx = idx if idx < 2 else 0 + else: + opbox.addItems(self.relationals) + opbox.setCurrentIndex(max(idx, 0)) + + def re_box_changed(self, state, line=None): + self.fill_in_opbox(line) + def empty_box_changed(self, state, line=None): if state == 2: self.tagboxes[line].setText('') @@ -472,6 +523,7 @@ class TagWizard(QDialog): else: self.reboxes[line].setEnabled(True) self.tagboxes[line].setEnabled(True) + self.fill_in_opbox(line) def and_box_changed(self, state, line=None): if state == 2: diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 7b9a2ac51c..805f22a503 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -523,7 +523,7 @@ class BuiltinFormat_date(BuiltinFormatterFunction): name = 'format_date' arg_count = 2 doc = _('format_date(val, format_string) -- format the value, which must ' - 'be a date field, using the format_string, returning a string. ' + 'be a date, using the format_string, returning a string. ' 'The formatting codes are: ' 'd : the day as number without a leading zero (1 to 31) ' 'dd : the day as number with a leading zero (01 to 31) ' @@ -538,7 +538,7 @@ class BuiltinFormat_date(BuiltinFormatterFunction): 'iso : the date with time and timezone. Must be the only format present') def evaluate(self, formatter, kwargs, mi, locals, val, format_string): - if not val: + if not val or val == 'None': return '' try: dt = parse_date(val) From b44a25174d5bfc0a1e3d780cefe4da616d679442 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 1 Jun 2011 06:46:01 +0100 Subject: [PATCH 03/17] Change name of BuiltinFormat_date to use camel case (BuiltinFormatDate) --- src/calibre/manual/template_ref.rst | 2 +- src/calibre/utils/formatter_functions.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/manual/template_ref.rst b/src/calibre/manual/template_ref.rst index 670a7ba791..de6c1fdb2c 100644 --- a/src/calibre/manual/template_ref.rst +++ b/src/calibre/manual/template_ref.rst @@ -34,7 +34,7 @@ booksize() format_date(val, format_string) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. autoclass:: BuiltinFormat_date +.. autoclass:: BuiltinFormatDate ondevice() ^^^^^^^^^^^ diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 755bb05cf1..b3e164ce9e 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -520,7 +520,7 @@ class BuiltinSubitems(BuiltinFormatterFunction): pass return ', '.join(sorted(rv, key=sort_key)) -class BuiltinFormat_date(BuiltinFormatterFunction): +class BuiltinFormatDate(BuiltinFormatterFunction): name = 'format_date' arg_count = 2 __doc__ = doc = _('format_date(val, format_string) -- format the value, ' @@ -704,7 +704,7 @@ builtin_divide = BuiltinDivide() builtin_eval = BuiltinEval() builtin_first_non_empty = BuiltinFirstNonEmpty() builtin_field = BuiltinField() -builtin_format_date = BuiltinFormat_date() +builtin_format_date = BuiltinFormatDate() builtin_ifempty = BuiltinIfempty() builtin_in_list = BuiltinInList() builtin_list_item = BuiltinListitem() From 88da0772f0522ebf726a9852cf559379e59ed5cd Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 1 Jun 2011 16:38:57 +0100 Subject: [PATCH 04/17] Fix regression in templates where id (and other non-standard attributes stuffed into mi) no longer work. --- src/calibre/ebooks/metadata/book/base.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 69407dcb2e..2c96b8d3d3 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -44,11 +44,16 @@ class SafeFormat(TemplateFormatter): def get_value(self, orig_key, args, kwargs): if not orig_key: return '' - key = orig_key.lower() + orig_key = orig_key.lower() + key = orig_key if key != 'title_sort' and key not in TOP_LEVEL_IDENTIFIERS: key = field_metadata.search_term_to_field_key(key) - if key is None or (self.book and key not in self.book.all_field_keys()): - raise ValueError(_('Value: unknown field ') + orig_key) + if key is None or (self.book and + key not in self.book.all_field_keys()): + if hasattr(self.book, orig_key): + key = orig_key + else: + raise ValueError(_('Value: unknown field ') + orig_key) b = self.book.get_user_metadata(key, False) if b and b['datatype'] == 'int' and self.book.get(key, 0) == 0: v = '' From da66d40ea95b4765b614d6a54e90aea270e4d55c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2011 10:38:22 -0600 Subject: [PATCH 05/17] Fix #789990 (Series index overwritten when series download turned off) --- src/calibre/ebooks/metadata/sources/identify.py | 2 ++ src/calibre/gui2/actions/edit_metadata.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py index 0cc070c3c6..303bb2db6e 100644 --- a/src/calibre/ebooks/metadata/sources/identify.py +++ b/src/calibre/ebooks/metadata/sources/identify.py @@ -408,6 +408,8 @@ def identify(log, abort, # {{{ for f in plugin.prefs['ignore_fields']: if ':' not in f: setattr(result, f, getattr(dummy, f)) + if f == 'series': + result.series_index = dummy.series_index result.relevance_in_source = i result.has_cached_cover_url = (plugin.cached_cover_url_is_reliable and plugin.get_cached_cover_url(result.identifiers) is not diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index ac475cb027..384bf4c9be 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -482,6 +482,8 @@ class EditMetadataAction(InterfaceAction): if mi.identifiers: idents.update(mi.identifiers) mi.identifiers = idents + if mi.is_null('series'): + mi.series_index = None db.set_metadata(i, mi, commit=False, set_title=set_title, set_authors=set_authors, notify=False) self.applied_ids.append(i) From efac88d69afa869b94b805292c6a14af0d4bf592 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 1 Jun 2011 17:43:31 +0100 Subject: [PATCH 06/17] Permit zero-argument template functions. --- .../gui2/preferences/template_functions.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/preferences/template_functions.py b/src/calibre/gui2/preferences/template_functions.py index fcb4c87372..7f310dace0 100644 --- a/src/calibre/gui2/preferences/template_functions.py +++ b/src/calibre/gui2/preferences/template_functions.py @@ -7,7 +7,9 @@ __docformat__ = 'restructuredtext en' import json, traceback -from calibre.gui2 import error_dialog +from PyQt4.Qt import QDialogButtonBox + +from calibre.gui2 import error_dialog, warning_dialog from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences.template_functions_ui import Ui_Form from calibre.gui2.widgets import PythonHighlighter @@ -152,10 +154,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): _('Name already used'), show=True) return if self.argument_count.value() == 0: - error_dialog(self.gui, _('Template functions'), - _('Argument count must be -1 or greater than zero'), - show=True) - return + box = warning_dialog(self.gui, _('Template functions'), + _('Argument count should be -1 or greater than zero.' + 'Setting it to zero means that this function cannot ' + 'be used in single function mode.'), det_msg = '', + show=False) + box.bb.setStandardButtons(box.bb.standardButtons() | QDialogButtonBox.Cancel) + box.det_msg_toggle.setVisible(False) + if not box.exec_(): + return try: prog = unicode(self.program.toPlainText()) cls = compile_user_function(name, unicode(self.documentation.toPlainText()), From 8769908316c095d831babdbf567c5edbe5e487e5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2011 12:34:35 -0600 Subject: [PATCH 07/17] Implement the wizard to create basic column coloring rules --- src/calibre/gui2/library/coloring.py | 500 +++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 src/calibre/gui2/library/coloring.py diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/library/coloring.py new file mode 100644 index 0000000000..64f5255780 --- /dev/null +++ b/src/calibre/gui2/library/coloring.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) +from future_builtins import map + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import json, binascii, re +from textwrap import dedent + +from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox, + QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon, + QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox) + +from calibre.utils.icu import sort_key +from calibre.gui2 import error_dialog + +class Rule(object): # {{{ + + SIGNATURE = '# BasicColorRule():' + + def __init__(self, fm): + self.color = None + self.fm = fm + self.conditions = [] + + def add_condition(self, col, action, val): + if col not in self.fm: + raise ValueError('%r is not a valid column name'%col) + v = self.validate_condition(col, action, val) + if v: + raise ValueError(v) + self.conditions.append((col, action, val)) + + def validate_condition(self, col, action, val): + m = self.fm[col] + dt = m['datatype'] + if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')): + try: + int(val) if dt == 'int' else float(val) + except: + return '%r is not a valid numerical value'%val + + if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern' + in action): + try: + re.compile(val) + except: + return '%r is not a valid regular expression'%val + + @property + def signature(self): + args = (self.color, self.conditions) + sig = json.dumps(args, ensure_ascii=False) + return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8')) + + @property + def template(self): + if not self.color or not self.conditions: + return None + conditions = map(self.apply_condition, self.conditions) + conditions = (',\n' + ' '*9).join(conditions) + return dedent('''\ + program: + {sig} + test(and('1', + {conditions} + ), {color}, '') + ''').format(sig=self.signature, conditions=conditions, + color=self.color) + + def apply_condition(self, condition): + col, action, val = condition + m = self.fm[col] + dt = m['datatype'] + + if dt == 'bool': + return self.bool_condition(col, action, val) + + if dt in ('int', 'float', 'rating'): + return self.number_condition(col, action, val) + + if dt == 'datetime': + return self.date_condition(col, action, val) + + if dt in ('comments', 'series', 'text', 'enumeration'): + ism = m.get('is_multiple', False) + if ism: + return self.multiple_condition(col, action, val, ism) + return self.text_condition(col, action, val) + + def bool_condition(self, col, action, val): + test = {'is true': 'True', + 'is false': 'False', + 'is undefined': 'None'}[action] + return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col) + + def number_condition(self, col, action, val): + lt, eq, gt = { + 'eq': ('', '1', ''), + 'lt': ('1', '', ''), + 'gt': ('', '', '1') + }[action] + lt, eq, gt = '', '1', '' + return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt) + + def date_condition(self, col, action, val): + lt, eq, gt = { + 'eq': ('', '1', ''), + 'lt': ('1', '', ''), + 'gt': ('', '', '1') + }[action] + return "cmp(format_date('%s', 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col, + val, lt, eq, gt) + + def multiple_condition(self, col, action, val, sep): + if action == 'is set': + return "test('%s', '1', '')"%col + if action == 'is not set': + return "test('%s', '', '1')"%col + if action == 'has': + return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val) + if action == 'does not have': + return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val) + if action == 'has pattern': + return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val) + if action == 'does not have pattern': + return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val) + + def text_condition(self, col, action, val): + if action == 'is set': + return "test('%s', '1', '')"%col + if action == 'is not set': + return "test('%s', '', '1')"%col + if action == 'is': + return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val) + if action == 'is not': + return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val) + if action == 'matches pattern': + return "contains(field('%s'), \"%s\", '1', '')"%(col, val) + if action == 'does not match pattern': + return "contains(field('%s'), \"%s\", '', '1')"%(col, val) + +# }}} + +def rule_from_template(fm, template): + ok_lines = [] + for line in template.splitlines(): + if line.startswith(Rule.SIGNATURE): + raw = line[len(Rule.SIGNATURE):].strip() + try: + color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8')) + except: + continue + r = Rule(fm) + r.color = color + for c in conditions: + try: + r.add_condition(*c) + except: + continue + if r.color and r.conditions: + return r + else: + ok_lines.append(line) + return '\n'.join(ok_lines) + +def conditionable_columns(fm): + for key in fm: + m = fm[key] + dt = m['datatype'] + if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series', + 'comments', 'text', 'enumeration', 'datetime'): + yield key + + +def displayable_columns(fm): + for key in fm.displayable_field_keys(): + if key not in ('sort', 'author_sort', 'comments', 'formats', + 'identifiers', 'path'): + yield key + +class ConditionEditor(QWidget): + + def __init__(self, fm, parent=None): + QWidget.__init__(self, parent) + self.fm = fm + + self.action_map = { + 'bool' : ( + (_('is true'), 'is true',), + (_('is false'), 'is false'), + (_('is undefined'), 'is undefined') + ), + 'int' : ( + (_('is equal to'), 'eq'), + (_('is less than'), 'lt'), + (_('is greater than'), 'gt') + ), + 'multiple' : ( + (_('has'), 'has'), + (_('does not have'), 'does not have'), + (_('has pattern'), 'has pattern'), + (_('does not have pattern'), 'does not have pattern'), + (_('is set'), 'is set'), + (_('is not set'), 'is not set'), + ), + 'single' : ( + (_('is'), 'is'), + (_('is not'), 'is not'), + (_('matches pattern'), 'matches pattern'), + (_('does not match pattern'), 'does not match pattern'), + (_('is set'), 'is set'), + (_('is not set'), 'is not set'), + ), + } + + for x in ('float', 'rating', 'datetime'): + self.action_map[x] = self.action_map['int'] + + self.l = l = QGridLayout(self) + self.setLayout(l) + + self.l1 = l1 = QLabel(_('If the ')) + l.addWidget(l1, 0, 0) + + self.column_box = QComboBox(self) + l.addWidget(self.column_box, 0, 1) + + self.l2 = l2 = QLabel(_(' column ')) + l.addWidget(l2, 0, 2) + + self.action_box = QComboBox(self) + l.addWidget(self.action_box, 0, 3) + + self.l3 = l3 = QLabel(_(' the value ')) + l.addWidget(l3, 0, 4) + + self.value_box = QLineEdit(self) + l.addWidget(self.value_box, 0, 5) + + self.column_box.addItem('', '') + for key in sorted( + conditionable_columns(fm), + key=lambda x:sort_key(fm[x]['name'])): + self.column_box.addItem(fm[key]['name'], key) + self.column_box.setCurrentIndex(0) + + self.column_box.currentIndexChanged.connect(self.init_action_box) + self.action_box.currentIndexChanged.connect(self.init_value_box) + + for b in (self.column_box, self.action_box): + b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon) + b.setMinimumContentsLength(15) + + @dynamic_property + def current_col(self): + def fget(self): + idx = self.column_box.currentIndex() + return unicode(self.column_box.itemData(idx).toString()) + def fset(self, val): + for idx in range(self.column_box.count()): + c = unicode(self.column_box.itemData(idx).toString()) + if c == val: + self.column_box.setCurrentIndex(idx) + return + raise ValueError('Column %r not found'%val) + return property(fget=fget, fset=fset) + + @dynamic_property + def current_action(self): + def fget(self): + idx = self.action_box.currentIndex() + return unicode(self.action_box.itemData(idx).toString()) + def fset(self, val): + for idx in range(self.action_box.count()): + c = unicode(self.action_box.itemData(idx).toString()) + if c == val: + self.action_box.setCurrentIndex(idx) + return + raise ValueError('Action %r not valid for current column'%val) + return property(fget=fget, fset=fset) + + @property + def current_val(self): + return unicode(self.value_box.text()).strip() + + @dynamic_property + def condition(self): + + def fget(self): + c, a, v = (self.current_col, self.current_action, + self.current_val) + if not c or not a: + return None + return (c, a, v) + + def fset(self, condition): + c, a, v = condition + if not v: + v = '' + v = v.strip() + self.current_col = c + self.current_action = a + self.value_box.setText(v) + + return property(fget=fget, fset=fset) + + def init_action_box(self): + self.action_box.blockSignals(True) + self.action_box.clear() + self.action_box.addItem('', '') + col = self.current_col + m = self.fm[col] + dt = m['datatype'] + if dt in self.action_map: + actions = self.action_map[dt] + else: + k = 'multiple' if m['is_multiple'] else 'single' + actions = self.action_map[k] + + for text, key in actions: + self.action_box.addItem(text, key) + self.action_box.setCurrentIndex(0) + self.action_box.blockSignals(False) + self.init_value_box() + + def init_value_box(self): + self.value_box.setEnabled(True) + self.value_box.setText('') + self.value_box.setInputMask('') + self.value_box.setValidator(None) + col = self.current_col + m = self.fm[col] + dt = m['datatype'] + action = self.current_action + if not col or not action: + return + tt = '' + if dt in ('int', 'float', 'rating'): + tt = _('Enter a number') + v = QIntValidator if dt == 'int' else QDoubleValidator + self.value_box.setValidator(v(self.value_box)) + elif dt == 'datetime': + self.value_box.setInputMask('9999-99-99') + tt = _('Enter a date in the format YYYY-MM-DD') + else: + tt = _('Enter a string') + if 'pattern' in action: + tt = _('Enter a regular expression') + self.value_box.setToolTip(tt) + if action in ('is set', 'is not set'): + self.value_box.setEnabled(False) + + +class RuleEditor(QDialog): + + def __init__(self, fm, parent=None): + QDialog.__init__(self, parent) + self.fm = fm + + self.setWindowIcon(QIcon(I('format-fill-color.png'))) + self.setWindowTitle(_('Create/edit a column coloring rule')) + + self.l = l = QGridLayout(self) + self.setLayout(l) + + self.l1 = l1 = QLabel(_('Create a coloring rule by' + ' filling in the boxes below')) + l.addWidget(l1, 0, 0, 1, 4) + + self.f1 = QFrame(self) + self.f1.setFrameShape(QFrame.HLine) + l.addWidget(self.f1, 1, 0, 1, 4) + + self.l2 = l2 = QLabel(_('Set the color of the column:')) + l.addWidget(l2, 2, 0) + + self.column_box = QComboBox(self) + l.addWidget(self.column_box, 2, 1) + + self.l3 = l3 = QLabel(_('to')) + l3.setAlignment(Qt.AlignHCenter) + l.addWidget(l3, 2, 2) + + self.color_box = QComboBox(self) + l.addWidget(self.color_box, 2, 3) + + self.l4 = l4 = QLabel( + _('Only if the following conditions are all satisfied:')) + l4.setAlignment(Qt.AlignHCenter) + l.addWidget(l4, 3, 0, 1, 4) + + self.scroll_area = sa = QScrollArea(self) + sa.setMinimumHeight(300) + sa.setMinimumWidth(950) + sa.setWidgetResizable(True) + l.addWidget(sa, 4, 0, 1, 4) + + self.add_button = b = QPushButton(QIcon(I('plus.png')), + _('Add another condition')) + l.addWidget(b, 5, 0, 1, 4) + b.clicked.connect(self.add_blank_condition) + + self.l5 = l5 = QLabel(_('You can disable a condition by' + ' blanking all of its boxes')) + l.addWidget(l5, 6, 0, 1, 4) + + self.bb = bb = QDialogButtonBox( + QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + l.addWidget(bb, 7, 0, 1, 4) + + self.conditions_widget = QWidget(self) + sa.setWidget(self.conditions_widget) + self.conditions_widget.setLayout(QVBoxLayout()) + self.conditions = [] + + for b in (self.column_box, self.color_box): + b.setSizeAdjustPolicy(b.AdjustToMinimumContentsLengthWithIcon) + b.setMinimumContentsLength(15) + + for key in sorted( + displayable_columns(fm), + key=lambda x:sort_key(fm[x]['name'])): + name = fm[key]['name'] + if name: + self.column_box.addItem(name, key) + self.column_box.setCurrentIndex(0) + + self.color_box.addItems(QColor.colorNames()) + self.color_box.setCurrentIndex(0) + + self.resize(self.sizeHint()) + + def add_blank_condition(self): + c = ConditionEditor(self.fm, parent=self.conditions_widget) + self.conditions.append(c) + self.conditions_widget.layout().addWidget(c) + + def accept(self): + if self.validate(): + QDialog.accept(self) + + def validate(self): + r = Rule(self.fm) + for c in self.conditions: + condition = c.condition + if condition is not None: + try: + r.add_condition(*condition) + except Exception as e: + import traceback + error_dialog(self, _('Invalid condition'), + _('One of the conditions for this rule is' + ' invalid: %s')%e, + det_msg=traceback.format_exc(), show=True) + return False + if len(r.conditions) < 1: + error_dialog(self, _('No conditions'), + _('You must specify at least one non-empty condition' + ' for this rule'), show=True) + return False + return True + + @property + def rule(self): + r = Rule(self.fm) + r.color = unicode(self.color_box.currentText()) + idx = self.column_box.currentIndex() + col = unicode(self.column_box.itemData(idx).toString()) + for c in self.conditions: + condition = c.condition + if condition is not None: + r.add_condition(*condition) + + return col, r + + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + + from calibre.library import db + + d = RuleEditor(db().field_metadata) + d.add_blank_condition() + d.exec_() + + col, r = d.rule + + print ('Column to be colored:', col) + print ('Template:') + print (r.template) + From bb47a2eb59dcfce7ab390dc4258a00649289a5a7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2011 12:37:03 -0600 Subject: [PATCH 08/17] ... --- src/calibre/gui2/library/coloring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/library/coloring.py index 64f5255780..9dd284481b 100644 --- a/src/calibre/gui2/library/coloring.py +++ b/src/calibre/gui2/library/coloring.py @@ -68,7 +68,7 @@ class Rule(object): # {{{ {sig} test(and('1', {conditions} - ), {color}, '') + ), {color}, ''); ''').format(sig=self.signature, conditions=conditions, color=self.color) From 9dac9bfbc31f7f387856197a33f9dd85fdbb5d5d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 1 Jun 2011 19:44:08 +0100 Subject: [PATCH 09/17] Beginning of automatic generation of template_ref.rst --- src/calibre/manual/template_ref_generate.py | 93 +++++++++++++++++++++ src/calibre/utils/formatter_functions.py | 47 ++++++++++- 2 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/calibre/manual/template_ref_generate.py diff --git a/src/calibre/manual/template_ref_generate.py b/src/calibre/manual/template_ref_generate.py new file mode 100644 index 0000000000..8618eb9f07 --- /dev/null +++ b/src/calibre/manual/template_ref_generate.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal ' + +from collections import defaultdict + +PREAMBLE = '''\ +.. include:: global.rst + +.. _templaterefcalibre: + +Reference for all builtin template language functions +======================================================== + +Here, we document all the builtin functions available in the |app| template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type. + +.. contents:: + :depth: 2 + :local: + +.. module:: calibre.utils.formatter_functions + +''' + +CATEGORY_TEMPLATE = '''\ +{category} +{dashes} + +''' + +FUNCTION_TEMPLATE = '''\ +{fs} +{hats} + +.. autoclass:: {cn} + +''' + +POSTAMBLE = '''\ + +API of the Metadata objects +---------------------------- + +The python implementation of the template functions is passed in a Metadata object. Knowing it's API is useful if you want to define your own template functions. + +.. module:: calibre.ebooks.metadata.book.base + +.. autoclass:: Metadata + :members: + :member-order: bysource + +.. data:: STANDARD_METADATA_FIELDS + + The set of standard metadata fields. + +.. literalinclude:: ../ebooks/metadata/book/__init__.py + :lines: 7- +''' + + +def generate_template_language_help(): + from calibre.utils.formatter_functions import all_builtin_functions + + funcs = defaultdict(dict) + + for func in all_builtin_functions: + class_name = func.__class__.__name__ + func_sig = getattr(func, 'doc') + x = func_sig.find(' -- ') + if x < 0: + print 'No sig for ', class_name + continue + func_sig = func_sig[:x] + func_cat = getattr(func, 'category') + funcs[func_cat][func_sig] = class_name + + output = PREAMBLE + cats = sorted(funcs.keys()) + for cat in cats: + output += CATEGORY_TEMPLATE.format(category=cat, dashes='-'*len(cat)) + entries = [k for k in sorted(funcs[cat].keys())] + for entry in entries: + output += FUNCTION_TEMPLATE.format(fs = entry, cn=funcs[cat][entry], + hats='^'*len(entry)) + + output += POSTAMBLE + print output + return output # and hope that something good happens to it + +if __name__ == '__main__': + generate_template_language_help() \ No newline at end of file diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index b3e164ce9e..78ed4fa306 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -57,6 +57,7 @@ class FormatterFunction(object): doc = _('No documentation provided') name = 'no name provided' + category = 'Unknown' arg_count = 0 def evaluate(self, formatter, kwargs, mi, locals, *args): @@ -87,6 +88,7 @@ class BuiltinFormatterFunction(FormatterFunction): class BuiltinStrcmp(BuiltinFormatterFunction): name = 'strcmp' arg_count = 5 + category = 'Relational' __doc__ = doc = _('strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x ' 'and y as strings. Returns lt if x < y. Returns eq if x == y. ' 'Otherwise returns gt.') @@ -101,6 +103,7 @@ class BuiltinStrcmp(BuiltinFormatterFunction): class BuiltinCmp(BuiltinFormatterFunction): name = 'cmp' + category = 'Relational' arg_count = 5 __doc__ = doc = _('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.') @@ -117,6 +120,7 @@ class BuiltinCmp(BuiltinFormatterFunction): class BuiltinStrcat(BuiltinFormatterFunction): name = 'strcat' arg_count = -1 + category = 'String Manipulation' __doc__ = doc = _('strcat(a, b, ...) -- can take any number of arguments. Returns a ' 'string formed by concatenating all the arguments') @@ -130,6 +134,7 @@ class BuiltinStrcat(BuiltinFormatterFunction): class BuiltinAdd(BuiltinFormatterFunction): name = 'add' arg_count = 2 + category = 'Arithmetic' __doc__ = doc = _('add(x, y) -- returns x + y. Throws an exception if either x or y are not numbers.') def evaluate(self, formatter, kwargs, mi, locals, x, y): @@ -140,6 +145,7 @@ class BuiltinAdd(BuiltinFormatterFunction): class BuiltinSubtract(BuiltinFormatterFunction): name = 'subtract' arg_count = 2 + category = 'Arithmetic' __doc__ = doc = _('subtract(x, y) -- returns x - y. Throws an exception if either x or y are not numbers.') def evaluate(self, formatter, kwargs, mi, locals, x, y): @@ -150,6 +156,7 @@ class BuiltinSubtract(BuiltinFormatterFunction): class BuiltinMultiply(BuiltinFormatterFunction): name = 'multiply' arg_count = 2 + category = 'Arithmetic' __doc__ = doc = _('multiply(x, y) -- returns x * y. Throws an exception if either x or y are not numbers.') def evaluate(self, formatter, kwargs, mi, locals, x, y): @@ -160,6 +167,7 @@ class BuiltinMultiply(BuiltinFormatterFunction): class BuiltinDivide(BuiltinFormatterFunction): name = 'divide' arg_count = 2 + category = 'Arithmetic' __doc__ = doc = _('divide(x, y) -- returns x / y. Throws an exception if either x or y are not numbers.') def evaluate(self, formatter, kwargs, mi, locals, x, y): @@ -170,6 +178,8 @@ class BuiltinDivide(BuiltinFormatterFunction): class BuiltinTemplate(BuiltinFormatterFunction): name = 'template' arg_count = 1 + category = 'Recursion' + __doc__ = doc = _('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 } ' @@ -185,6 +195,7 @@ class BuiltinTemplate(BuiltinFormatterFunction): class BuiltinEval(BuiltinFormatterFunction): name = 'eval' arg_count = 1 + category = 'Recursion' __doc__ = doc = _('eval(template) -- evaluates the template, passing the local ' 'variables (those \'assign\'ed to) instead of the book metadata. ' ' This permits using the template processor to construct complex ' @@ -198,6 +209,7 @@ class BuiltinEval(BuiltinFormatterFunction): class BuiltinAssign(BuiltinFormatterFunction): name = 'assign' arg_count = 2 + category = 'Other' __doc__ = doc = _('assign(id, val) -- assigns val to id, then returns val. ' 'id must be an identifier, not an expression') @@ -208,6 +220,7 @@ class BuiltinAssign(BuiltinFormatterFunction): class BuiltinPrint(BuiltinFormatterFunction): name = 'print' arg_count = -1 + category = 'Other' __doc__ = doc = _('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.') @@ -219,14 +232,16 @@ class BuiltinPrint(BuiltinFormatterFunction): class BuiltinField(BuiltinFormatterFunction): name = 'field' arg_count = 1 + category = 'Get values from metadata' __doc__ = doc = _('field(name) -- returns the metadata field named by name') def evaluate(self, formatter, kwargs, mi, locals, name): return formatter.get_value(name, [], kwargs) -class BuiltinRaw_field(BuiltinFormatterFunction): +class BuiltinRawField(BuiltinFormatterFunction): name = 'raw_field' arg_count = 1 + category = 'Get values from metadata' __doc__ = doc = _('raw_field(name) -- returns the metadata field named by name ' 'without applying any formatting.') @@ -236,6 +251,7 @@ class BuiltinRaw_field(BuiltinFormatterFunction): class BuiltinSubstr(BuiltinFormatterFunction): name = 'substr' arg_count = 3 + category = 'String Manipulation' __doc__ = doc = _('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 ' @@ -249,6 +265,7 @@ class BuiltinSubstr(BuiltinFormatterFunction): class BuiltinLookup(BuiltinFormatterFunction): name = 'lookup' arg_count = -1 + category = 'Iterating over values' __doc__ = doc = _('lookup(val, pattern, field, pattern, field, ..., else_field) -- ' 'like switch, except the arguments are field (metadata) names, not ' 'text. The value of the appropriate field will be fetched and used. ' @@ -276,6 +293,7 @@ class BuiltinLookup(BuiltinFormatterFunction): class BuiltinTest(BuiltinFormatterFunction): name = 'test' arg_count = 3 + category = 'If-then-else' __doc__ = doc = _('test(val, text if not empty, text if empty) -- return `text if not ' 'empty` if the field is not empty, otherwise return `text if empty`') @@ -288,6 +306,7 @@ class BuiltinTest(BuiltinFormatterFunction): class BuiltinContains(BuiltinFormatterFunction): name = 'contains' arg_count = 4 + category = 'If-then-else' __doc__ = doc = _('contains(val, pattern, text if match, text if not match) -- checks ' 'if field contains matches for the regular expression `pattern`. ' 'Returns `text if match` if matches are found, otherwise it returns ' @@ -303,6 +322,7 @@ class BuiltinContains(BuiltinFormatterFunction): class BuiltinSwitch(BuiltinFormatterFunction): name = 'switch' arg_count = -1 + category = 'Iterating over values' __doc__ = doc = _('switch(val, pattern, value, pattern, value, ..., else_value) -- ' 'for each `pattern, value` pair, checks if the field matches ' 'the regular expression `pattern` and if so, returns that ' @@ -323,6 +343,7 @@ class BuiltinSwitch(BuiltinFormatterFunction): class BuiltinInList(BuiltinFormatterFunction): name = 'in_list' arg_count = 5 + category = 'List Lookup' __doc__ = doc = _('in_list(val, separator, pattern, found_val, not_found_val) -- ' 'treat val as a list of items separated by separator, ' 'comparing the pattern against each value in the list. If the ' @@ -340,6 +361,8 @@ class BuiltinInList(BuiltinFormatterFunction): class BuiltinStrInList(BuiltinFormatterFunction): name = 'str_in_list' arg_count = 5 + category = 'List Lookup' + category = 'Iterating over values' __doc__ = doc = _('str_in_list(val, separator, string, found_val, not_found_val) -- ' 'treat val as a list of items separated by separator, ' 'comparing the string against each value in the list. If the ' @@ -360,6 +383,7 @@ class BuiltinStrInList(BuiltinFormatterFunction): class BuiltinRe(BuiltinFormatterFunction): name = 're' arg_count = 3 + category = 'String Manipulation' __doc__ = doc = _('re(val, pattern, replacement) -- return the field after applying ' 'the regular expression. All instances of `pattern` are replaced ' 'with `replacement`. As in all of calibre, these are ' @@ -371,6 +395,7 @@ class BuiltinRe(BuiltinFormatterFunction): class BuiltinIfempty(BuiltinFormatterFunction): name = 'ifempty' arg_count = 2 + category = 'If-then-else' __doc__ = doc = _('ifempty(val, text if empty) -- return val if val is not empty, ' 'otherwise return `text if empty`') @@ -383,6 +408,7 @@ class BuiltinIfempty(BuiltinFormatterFunction): class BuiltinShorten(BuiltinFormatterFunction): name = 'shorten' arg_count = 4 + category = 'String Manipulation' __doc__ = doc = _('shorten(val, left chars, middle text, right chars) -- Return a ' 'shortened version of the field, consisting of `left chars` ' 'characters from the beginning of the field, followed by ' @@ -408,6 +434,7 @@ class BuiltinShorten(BuiltinFormatterFunction): class BuiltinCount(BuiltinFormatterFunction): name = 'count' arg_count = 2 + category = 'List Manipulation' __doc__ = doc = _('count(val, separator) -- interprets the value as a list of items ' 'separated by `separator`, returning the number of items in the ' 'list. Most lists use a comma as the separator, but authors ' @@ -419,6 +446,7 @@ class BuiltinCount(BuiltinFormatterFunction): class BuiltinListitem(BuiltinFormatterFunction): name = 'list_item' arg_count = 3 + category = 'List Lookup' __doc__ = doc = _('list_item(val, index, separator) -- interpret the value as a list of ' 'items separated by `separator`, returning the `index`th item. ' 'The first item is number zero. The last item can be returned ' @@ -439,6 +467,7 @@ class BuiltinListitem(BuiltinFormatterFunction): class BuiltinSelect(BuiltinFormatterFunction): name = 'select' arg_count = 2 + category = 'List Lookup' __doc__ = doc = _('select(val, key) -- interpret the value as a comma-separated list ' 'of items, with the items being "id:value". Find the pair with the' 'id equal to key, and return the corresponding value.' @@ -456,6 +485,7 @@ class BuiltinSelect(BuiltinFormatterFunction): class BuiltinSublist(BuiltinFormatterFunction): name = 'sublist' arg_count = 4 + category = 'List Manipulation' __doc__ = doc = _('sublist(val, start_index, end_index, separator) -- interpret the ' 'value as a list of items separated by `separator`, returning a ' 'new list made from the `start_index` to the `end_index` item. ' @@ -486,6 +516,7 @@ class BuiltinSublist(BuiltinFormatterFunction): class BuiltinSubitems(BuiltinFormatterFunction): name = 'subitems' arg_count = 3 + category = 'List Manipulation' __doc__ = doc = _('subitems(val, start_index, end_index) -- This function is used to ' 'break apart lists of items such as genres. It interprets the value ' 'as a comma-separated list of items, where each item is a period-' @@ -523,6 +554,7 @@ class BuiltinSubitems(BuiltinFormatterFunction): class BuiltinFormatDate(BuiltinFormatterFunction): name = 'format_date' arg_count = 2 + category = 'Get values from metadata' __doc__ = doc = _('format_date(val, format_string) -- format the value, ' 'which must be a date, using the format_string, returning a string. ' 'The formatting codes are: ' @@ -551,6 +583,7 @@ class BuiltinFormatDate(BuiltinFormatterFunction): class BuiltinUppercase(BuiltinFormatterFunction): name = 'uppercase' arg_count = 1 + category = 'String case changes' __doc__ = doc = _('uppercase(val) -- return value of the field in upper case') def evaluate(self, formatter, kwargs, mi, locals, val): @@ -559,6 +592,7 @@ class BuiltinUppercase(BuiltinFormatterFunction): class BuiltinLowercase(BuiltinFormatterFunction): name = 'lowercase' arg_count = 1 + category = 'String case changes' __doc__ = doc = _('lowercase(val) -- return value of the field in lower case') def evaluate(self, formatter, kwargs, mi, locals, val): @@ -567,6 +601,7 @@ class BuiltinLowercase(BuiltinFormatterFunction): class BuiltinTitlecase(BuiltinFormatterFunction): name = 'titlecase' arg_count = 1 + category = 'String case changes' __doc__ = doc = _('titlecase(val) -- return value of the field in title case') def evaluate(self, formatter, kwargs, mi, locals, val): @@ -575,6 +610,7 @@ class BuiltinTitlecase(BuiltinFormatterFunction): class BuiltinCapitalize(BuiltinFormatterFunction): name = 'capitalize' arg_count = 1 + category = 'String case changes' __doc__ = doc = _('capitalize(val) -- return value of the field capitalized') def evaluate(self, formatter, kwargs, mi, locals, val): @@ -583,6 +619,7 @@ class BuiltinCapitalize(BuiltinFormatterFunction): class BuiltinBooksize(BuiltinFormatterFunction): name = 'booksize' arg_count = 0 + category = 'Get values from metadata' __doc__ = doc = _('booksize() -- return value of the size field') def evaluate(self, formatter, kwargs, mi, locals): @@ -596,6 +633,7 @@ class BuiltinBooksize(BuiltinFormatterFunction): class BuiltinOndevice(BuiltinFormatterFunction): name = 'ondevice' arg_count = 0 + category = 'Get values from metadata' __doc__ = doc = _('ondevice() -- return Yes if ondevice is set, otherwise return ' 'the empty string') @@ -607,6 +645,7 @@ class BuiltinOndevice(BuiltinFormatterFunction): class BuiltinFirstNonEmpty(BuiltinFormatterFunction): name = 'first_non_empty' arg_count = -1 + category = 'Iterating over values' __doc__ = doc = _('first_non_empty(value, value, ...) -- ' 'returns the first value that is not empty. If all values are ' 'empty, then the empty value is returned.' @@ -623,6 +662,7 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction): class BuiltinAnd(BuiltinFormatterFunction): name = 'and' arg_count = -1 + category = 'Boolean' __doc__ = 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 ' @@ -639,6 +679,7 @@ class BuiltinAnd(BuiltinFormatterFunction): class BuiltinOr(BuiltinFormatterFunction): name = 'or' arg_count = -1 + category = 'Boolean' __doc__ = 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 ' @@ -655,6 +696,7 @@ class BuiltinOr(BuiltinFormatterFunction): class BuiltinNot(BuiltinFormatterFunction): name = 'not' arg_count = 1 + category = 'Boolean' __doc__ = doc = _('not(value) -- ' 'returns the string "1" if the value is empty, otherwise ' 'returns the empty string. This function works well with test or ' @@ -671,6 +713,7 @@ class BuiltinNot(BuiltinFormatterFunction): class BuiltinMergeLists(BuiltinFormatterFunction): name = 'merge_lists' arg_count = 3 + category = 'List Manipulation' __doc__ = doc = _('merge_lists(list1, list2, separator) -- ' 'return a list made by merging the items in list1 and list2, ' 'removing duplicate items using a case-insensitive compare. If ' @@ -716,7 +759,7 @@ builtin_not = BuiltinNot() builtin_ondevice = BuiltinOndevice() builtin_or = BuiltinOr() builtin_print = BuiltinPrint() -builtin_raw_field = BuiltinRaw_field() +builtin_raw_field = BuiltinRawField() builtin_re = BuiltinRe() builtin_select = BuiltinSelect() builtin_shorten = BuiltinShorten() From a1afc8b7eb5dcb7f65d82e6e7252bb3b070afa2e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2011 14:23:09 -0600 Subject: [PATCH 10/17] ... --- src/calibre/gui2/library/coloring.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/library/coloring.py index 9dd284481b..7fb45d9ba6 100644 --- a/src/calibre/gui2/library/coloring.py +++ b/src/calibre/gui2/library/coloring.py @@ -66,7 +66,7 @@ class Rule(object): # {{{ return dedent('''\ program: {sig} - test(and('1', + test(and( {conditions} ), {color}, ''); ''').format(sig=self.signature, conditions=conditions, @@ -113,7 +113,7 @@ class Rule(object): # {{{ 'lt': ('1', '', ''), 'gt': ('', '', '1') }[action] - return "cmp(format_date('%s', 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col, + return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt) def multiple_condition(self, col, action, val, sep): @@ -246,7 +246,7 @@ class ConditionEditor(QWidget): for key in sorted( conditionable_columns(fm), key=lambda x:sort_key(fm[x]['name'])): - self.column_box.addItem(fm[key]['name'], key) + self.column_box.addItem(key, key) self.column_box.setCurrentIndex(0) self.column_box.currentIndexChanged.connect(self.init_action_box) @@ -352,7 +352,8 @@ class ConditionEditor(QWidget): if 'pattern' in action: tt = _('Enter a regular expression') self.value_box.setToolTip(tt) - if action in ('is set', 'is not set'): + if action in ('is set', 'is not set', 'is true', 'is false', + 'is undefined'): self.value_box.setEnabled(False) @@ -418,6 +419,7 @@ class RuleEditor(QDialog): self.conditions_widget = QWidget(self) sa.setWidget(self.conditions_widget) self.conditions_widget.setLayout(QVBoxLayout()) + self.conditions_widget.layout().setAlignment(Qt.AlignTop) self.conditions = [] for b in (self.column_box, self.color_box): @@ -429,7 +431,7 @@ class RuleEditor(QDialog): key=lambda x:sort_key(fm[x]['name'])): name = fm[key]['name'] if name: - self.column_box.addItem(name, key) + self.column_box.addItem(key, key) self.column_box.setCurrentIndex(0) self.color_box.addItems(QColor.colorNames()) From 739693060d2373cccf0f091fa7100842b3cb2ab2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2011 15:18:49 -0600 Subject: [PATCH 11/17] Autogen the template function docs --- src/calibre/manual/custom.py | 12 +- src/calibre/manual/template_ref.rst | 266 -------------------- src/calibre/manual/template_ref_generate.py | 5 +- 3 files changed, 13 insertions(+), 270 deletions(-) delete mode 100644 src/calibre/manual/template_ref.rst diff --git a/src/calibre/manual/custom.py b/src/calibre/manual/custom.py index f5db6dd0c2..4788889972 100644 --- a/src/calibre/manual/custom.py +++ b/src/calibre/manual/custom.py @@ -240,11 +240,21 @@ def cli_docs(app): raw += '\n'+'\n'.join(lines) update_cli_doc(os.path.join('cli', cmd+'.rst'), raw, info) +def generate_docs(app): + cli_docs(app) + template_docs(app) + +def template_docs(app): + from template_ref_generate import generate_template_language_help + info = app.builder.info + raw = generate_template_language_help() + update_cli_doc('template_ref.rst', raw, info) + def setup(app): app.add_config_value('epub_cover', None, False) app.add_builder(EPUBHelpBuilder) app.connect('doctree-read', substitute) - app.connect('builder-inited', cli_docs) + app.connect('builder-inited', generate_docs) app.connect('build-finished', finished) def finished(app, exception): diff --git a/src/calibre/manual/template_ref.rst b/src/calibre/manual/template_ref.rst deleted file mode 100644 index de6c1fdb2c..0000000000 --- a/src/calibre/manual/template_ref.rst +++ /dev/null @@ -1,266 +0,0 @@ -.. include:: global.rst - -.. _templaterefcalibre: - -Reference for all builtin template language functions -======================================================== - -Here, we document all the builtin functions available in the |app| template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type. - -.. contents:: - :depth: 2 - :local: - -.. module:: calibre.utils.formatter_functions - -Get values from metadata --------------------------- - -field(name) -^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinField - -raw_field(name) -^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinRaw_field - -booksize() -^^^^^^^^^^^^ - -.. autoclass:: BuiltinBooksize - -format_date(val, format_string) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinFormatDate - -ondevice() -^^^^^^^^^^^ - -.. autoclass:: BuiltinOndevice - -Arithmetic -------------- - -add(x, y) -^^^^^^^^^^^^^ -.. autoclass:: BuiltinAdd - -subtract(x, y) -^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinSubtract - -multiply(x, y) -^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinMultiply - -divide(x, y) -^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinDivide - -Boolean ------------- - -and(value1, value2, ...) -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinAnd - -or(value1, value2, ...) -^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinOr - -not(value) -^^^^^^^^^^^^^ - -.. autoclass:: BuiltinNot - -If-then-else ------------------ - -contains(val, pattern, text if match, text if not match) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinContains - -test(val, text if not empty, text if empty) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinTest - -ifempty(val, text if empty) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinIfempty - -Iterating over values ------------------------- - -first_non_empty(value, value, ...) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinFirstNonEmpty - -lookup(val, pattern, field, pattern, field, ..., else_field) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinLookup - -switch(val, pattern, value, pattern, value, ..., else_value) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinSwitch - -List Lookup ---------------- - -in_list(val, separator, pattern, found_val, not_found_val) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinInList - -str_in_list(val, separator, string, found_val, not_found_val) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinStrInList - -list_item(val, index, separator) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinListitem - -select(val, key) -^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinSelect - - -List Manipulation -------------------- - -count(val, separator) -^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinCount - -merge_lists(list1, list2, separator) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinMergeLists - -sublist(val, start_index, end_index, separator) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinSublist - -subitems(val, start_index, end_index) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinSubitems - -Recursion -------------- - -eval(template) -^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinEval - -template(x) -^^^^^^^^^^^^ - -.. autoclass:: BuiltinTemplate - -Relational ------------ - -cmp(x, y, lt, eq, gt) -^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinCmp - -strcmp(x, y, lt, eq, gt) -^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinStrcmp - -String case changes ---------------------- - -lowercase(val) -^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinLowercase - -uppercase(val) -^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinUppercase - -titlecase(val) -^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinTitlecase - -capitalize(val) -^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinCapitalize - -String Manipulation ---------------------- - -re(val, pattern, replacement) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinRe - -shorten(val, left chars, middle text, right chars) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinShorten - -substr(str, start, end) -^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinSubstr - - -Other --------- - -assign(id, val) -^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinAssign - -print(a, b, ...) -^^^^^^^^^^^^^^^^^ - -.. autoclass:: BuiltinPrint - - -API of the Metadata objects ----------------------------- - -The python implementation of the template functions is passed in a Metadata object. Knowing it's API is useful if you want to define your own template functions. - -.. module:: calibre.ebooks.metadata.book.base - -.. autoclass:: Metadata - :members: - :member-order: bysource - -.. data:: STANDARD_METADATA_FIELDS - - The set of standard metadata fields. - -.. literalinclude:: ../ebooks/metadata/book/__init__.py - :lines: 7- - diff --git a/src/calibre/manual/template_ref_generate.py b/src/calibre/manual/template_ref_generate.py index 8618eb9f07..742ab1fd54 100644 --- a/src/calibre/manual/template_ref_generate.py +++ b/src/calibre/manual/template_ref_generate.py @@ -86,8 +86,7 @@ def generate_template_language_help(): hats='^'*len(entry)) output += POSTAMBLE - print output - return output # and hope that something good happens to it + return output if __name__ == '__main__': - generate_template_language_help() \ No newline at end of file + generate_template_language_help() From cff2fcb6eb1caf0529159a65aa68d78aec5dd9da Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2011 15:19:24 -0600 Subject: [PATCH 12/17] ... --- .bzrignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.bzrignore b/.bzrignore index 005391bf46..d2a2d592dd 100644 --- a/.bzrignore +++ b/.bzrignore @@ -4,6 +4,7 @@ src/calibre/plugins resources/images.qrc src/calibre/manual/.build/ src/calibre/manual/cli/ +src/calibre/manual/template_ref.rst build dist docs @@ -31,4 +32,4 @@ nbproject/ .pydevproject .settings/ *.DS_Store -calibre_plugins/ \ No newline at end of file +calibre_plugins/ From 04c5fcc2eee8152840f723137497156dfa11b98b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2011 15:24:49 -0600 Subject: [PATCH 13/17] Split non-GUI part of the coloring code into a non GUI module --- src/calibre/gui2/library/coloring.py | 171 +------------------------ src/calibre/library/coloring.py | 178 +++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 169 deletions(-) create mode 100644 src/calibre/library/coloring.py diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/library/coloring.py index 7fb45d9ba6..4c8d678b19 100644 --- a/src/calibre/gui2/library/coloring.py +++ b/src/calibre/gui2/library/coloring.py @@ -2,186 +2,19 @@ # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import (unicode_literals, division, absolute_import, print_function) -from future_builtins import map __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import json, binascii, re -from textwrap import dedent - from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox, QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon, QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox) from calibre.utils.icu import sort_key from calibre.gui2 import error_dialog - -class Rule(object): # {{{ - - SIGNATURE = '# BasicColorRule():' - - def __init__(self, fm): - self.color = None - self.fm = fm - self.conditions = [] - - def add_condition(self, col, action, val): - if col not in self.fm: - raise ValueError('%r is not a valid column name'%col) - v = self.validate_condition(col, action, val) - if v: - raise ValueError(v) - self.conditions.append((col, action, val)) - - def validate_condition(self, col, action, val): - m = self.fm[col] - dt = m['datatype'] - if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')): - try: - int(val) if dt == 'int' else float(val) - except: - return '%r is not a valid numerical value'%val - - if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern' - in action): - try: - re.compile(val) - except: - return '%r is not a valid regular expression'%val - - @property - def signature(self): - args = (self.color, self.conditions) - sig = json.dumps(args, ensure_ascii=False) - return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8')) - - @property - def template(self): - if not self.color or not self.conditions: - return None - conditions = map(self.apply_condition, self.conditions) - conditions = (',\n' + ' '*9).join(conditions) - return dedent('''\ - program: - {sig} - test(and( - {conditions} - ), {color}, ''); - ''').format(sig=self.signature, conditions=conditions, - color=self.color) - - def apply_condition(self, condition): - col, action, val = condition - m = self.fm[col] - dt = m['datatype'] - - if dt == 'bool': - return self.bool_condition(col, action, val) - - if dt in ('int', 'float', 'rating'): - return self.number_condition(col, action, val) - - if dt == 'datetime': - return self.date_condition(col, action, val) - - if dt in ('comments', 'series', 'text', 'enumeration'): - ism = m.get('is_multiple', False) - if ism: - return self.multiple_condition(col, action, val, ism) - return self.text_condition(col, action, val) - - def bool_condition(self, col, action, val): - test = {'is true': 'True', - 'is false': 'False', - 'is undefined': 'None'}[action] - return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col) - - def number_condition(self, col, action, val): - lt, eq, gt = { - 'eq': ('', '1', ''), - 'lt': ('1', '', ''), - 'gt': ('', '', '1') - }[action] - lt, eq, gt = '', '1', '' - return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt) - - def date_condition(self, col, action, val): - lt, eq, gt = { - 'eq': ('', '1', ''), - 'lt': ('1', '', ''), - 'gt': ('', '', '1') - }[action] - return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col, - val, lt, eq, gt) - - def multiple_condition(self, col, action, val, sep): - if action == 'is set': - return "test('%s', '1', '')"%col - if action == 'is not set': - return "test('%s', '', '1')"%col - if action == 'has': - return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val) - if action == 'does not have': - return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val) - if action == 'has pattern': - return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val) - if action == 'does not have pattern': - return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val) - - def text_condition(self, col, action, val): - if action == 'is set': - return "test('%s', '1', '')"%col - if action == 'is not set': - return "test('%s', '', '1')"%col - if action == 'is': - return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val) - if action == 'is not': - return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val) - if action == 'matches pattern': - return "contains(field('%s'), \"%s\", '1', '')"%(col, val) - if action == 'does not match pattern': - return "contains(field('%s'), \"%s\", '', '1')"%(col, val) - -# }}} - -def rule_from_template(fm, template): - ok_lines = [] - for line in template.splitlines(): - if line.startswith(Rule.SIGNATURE): - raw = line[len(Rule.SIGNATURE):].strip() - try: - color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8')) - except: - continue - r = Rule(fm) - r.color = color - for c in conditions: - try: - r.add_condition(*c) - except: - continue - if r.color and r.conditions: - return r - else: - ok_lines.append(line) - return '\n'.join(ok_lines) - -def conditionable_columns(fm): - for key in fm: - m = fm[key] - dt = m['datatype'] - if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series', - 'comments', 'text', 'enumeration', 'datetime'): - yield key - - -def displayable_columns(fm): - for key in fm.displayable_field_keys(): - if key not in ('sort', 'author_sort', 'comments', 'formats', - 'identifiers', 'path'): - yield key +from calibre.library.coloring import (Rule, conditionable_columns, + displayable_columns) class ConditionEditor(QWidget): diff --git a/src/calibre/library/coloring.py b/src/calibre/library/coloring.py new file mode 100644 index 0000000000..7e2b0f67c6 --- /dev/null +++ b/src/calibre/library/coloring.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) +from future_builtins import map + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import binascii, re, json +from textwrap import dedent + +class Rule(object): # {{{ + + SIGNATURE = '# BasicColorRule():' + + def __init__(self, fm, color=None): + self.color = color + self.fm = fm + self.conditions = [] + + def add_condition(self, col, action, val): + if col not in self.fm: + raise ValueError('%r is not a valid column name'%col) + v = self.validate_condition(col, action, val) + if v: + raise ValueError(v) + self.conditions.append((col, action, val)) + + def validate_condition(self, col, action, val): + m = self.fm[col] + dt = m['datatype'] + if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')): + try: + int(val) if dt == 'int' else float(val) + except: + return '%r is not a valid numerical value'%val + + if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern' + in action): + try: + re.compile(val) + except: + return '%r is not a valid regular expression'%val + + @property + def signature(self): + args = (self.color, self.conditions) + sig = json.dumps(args, ensure_ascii=False) + return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8')) + + @property + def template(self): + if not self.color or not self.conditions: + return None + conditions = map(self.apply_condition, self.conditions) + conditions = (',\n' + ' '*9).join(conditions) + return dedent('''\ + program: + {sig} + test(and( + {conditions} + ), {color}, ''); + ''').format(sig=self.signature, conditions=conditions, + color=self.color) + + def apply_condition(self, condition): + col, action, val = condition + m = self.fm[col] + dt = m['datatype'] + + if dt == 'bool': + return self.bool_condition(col, action, val) + + if dt in ('int', 'float', 'rating'): + return self.number_condition(col, action, val) + + if dt == 'datetime': + return self.date_condition(col, action, val) + + if dt in ('comments', 'series', 'text', 'enumeration'): + ism = m.get('is_multiple', False) + if ism: + return self.multiple_condition(col, action, val, ism) + return self.text_condition(col, action, val) + + def bool_condition(self, col, action, val): + test = {'is true': 'True', + 'is false': 'False', + 'is undefined': 'None'}[action] + return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col) + + def number_condition(self, col, action, val): + lt, eq, gt = { + 'eq': ('', '1', ''), + 'lt': ('1', '', ''), + 'gt': ('', '', '1') + }[action] + lt, eq, gt = '', '1', '' + return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt) + + def date_condition(self, col, action, val): + lt, eq, gt = { + 'eq': ('', '1', ''), + 'lt': ('1', '', ''), + 'gt': ('', '', '1') + }[action] + return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col, + val, lt, eq, gt) + + def multiple_condition(self, col, action, val, sep): + if action == 'is set': + return "test('%s', '1', '')"%col + if action == 'is not set': + return "test('%s', '', '1')"%col + if action == 'has': + return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val) + if action == 'does not have': + return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val) + if action == 'has pattern': + return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val) + if action == 'does not have pattern': + return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val) + + def text_condition(self, col, action, val): + if action == 'is set': + return "test('%s', '1', '')"%col + if action == 'is not set': + return "test('%s', '', '1')"%col + if action == 'is': + return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val) + if action == 'is not': + return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val) + if action == 'matches pattern': + return "contains(field('%s'), \"%s\", '1', '')"%(col, val) + if action == 'does not match pattern': + return "contains(field('%s'), \"%s\", '', '1')"%(col, val) + +# }}} + +def rule_from_template(fm, template): + ok_lines = [] + for line in template.splitlines(): + if line.startswith(Rule.SIGNATURE): + raw = line[len(Rule.SIGNATURE):].strip() + try: + color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8')) + except: + continue + r = Rule(fm) + r.color = color + for c in conditions: + try: + r.add_condition(*c) + except: + continue + if r.color and r.conditions: + return r + else: + ok_lines.append(line) + return '\n'.join(ok_lines) + +def conditionable_columns(fm): + for key in fm: + m = fm[key] + dt = m['datatype'] + if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series', + 'comments', 'text', 'enumeration', 'datetime'): + yield key + + +def displayable_columns(fm): + for key in fm.displayable_field_keys(): + if key not in ('sort', 'author_sort', 'comments', 'formats', + 'identifiers', 'path'): + yield key + From fcc6ec5de59a7424eaf685c2c709530b4ec5b7c2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2011 15:28:54 -0600 Subject: [PATCH 14/17] ... --- src/calibre/gui2/library/coloring.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/library/coloring.py index 4c8d678b19..98ac7380a5 100644 --- a/src/calibre/gui2/library/coloring.py +++ b/src/calibre/gui2/library/coloring.py @@ -16,7 +16,7 @@ from calibre.gui2 import error_dialog from calibre.library.coloring import (Rule, conditionable_columns, displayable_columns) -class ConditionEditor(QWidget): +class ConditionEditor(QWidget): # {{{ def __init__(self, fm, parent=None): QWidget.__init__(self, parent) @@ -188,9 +188,9 @@ class ConditionEditor(QWidget): if action in ('is set', 'is not set', 'is true', 'is false', 'is undefined'): self.value_box.setEnabled(False) +# }}} - -class RuleEditor(QDialog): +class RuleEditor(QDialog): # {{{ def __init__(self, fm, parent=None): QDialog.__init__(self, parent) @@ -314,8 +314,13 @@ class RuleEditor(QDialog): r.add_condition(*condition) return col, r +# }}} +class EditRules(QWidget): + def __init__(self, db, parent=None): + QWidget.__init__(self, parent) + self.db = db if __name__ == '__main__': from PyQt4.Qt import QApplication From d559f7df719643ac46101ed4ce157ac0549ceae9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 1 Jun 2011 15:29:31 -0600 Subject: [PATCH 15/17] ... --- src/calibre/gui2/{library => preferences}/coloring.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/calibre/gui2/{library => preferences}/coloring.py (100%) diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/preferences/coloring.py similarity index 100% rename from src/calibre/gui2/library/coloring.py rename to src/calibre/gui2/preferences/coloring.py From fc8f268ee9ce4ce77f1c4050f2da554b0a226e03 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 1 Jun 2011 18:22:55 -0400 Subject: [PATCH 16/17] Store: EpubBud store. --- src/calibre/customize/builtins.py | 10 +++ src/calibre/gui2/store/epubbud_plugin.py | 81 ++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/calibre/gui2/store/epubbud_plugin.py diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 5cde30f72e..9ebec5e7e8 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1227,6 +1227,15 @@ class StoreEHarlequinStore(StoreBase): formats = ['EPUB', 'PDF'] affiliate = True +class StoreEpubBudStore(StoreBase): + name = 'ePub Bud' + description = 'Well, it\'s pretty much just "YouTube for Children\'s eBooks. A not-for-profit organization devoted to brining self published childrens books to the world.' + actual_plugin = 'calibre.gui2.store.epubbud_plugin:EpubBudStore' + + drm_free_only = True + headquarters = 'US' + formats = ['EPUB'] + class StoreFeedbooksStore(StoreBase): name = 'Feedbooks' description = u'Feedbooks is a cloud publishing and distribution service, connected to a large ecosystem of reading systems and social networks. Provides a variety of genres from independent and classic books.' @@ -1422,6 +1431,7 @@ plugins += [ StoreEBookShoppeUKStore, StoreEPubBuyDEStore, StoreEHarlequinStore, + StoreEpubBudStore, StoreFeedbooksStore, StoreFoylesUKStore, StoreGandalfStore, diff --git a/src/calibre/gui2/store/epubbud_plugin.py b/src/calibre/gui2/store/epubbud_plugin.py new file mode 100644 index 0000000000..6c20f5150d --- /dev/null +++ b/src/calibre/gui2/store/epubbud_plugin.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, John Schember ' +__docformat__ = 'restructuredtext en' + +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 EpubBudStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + url = 'http://epubbud.com/' + + if detail_item: + url = 'http://epubbud.com/book.php?g=' + detail_item + + 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_item) + d.setWindowTitle(self.name) + d.set_tags(self.config.get('tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + ''' + OPDS based search. + + We really should get the catelog from http://pragprog.com/catalog.opds + and look for the application/opensearchdescription+xml entry. + Then get the opensearch description to get the search url and + format. However, we are going to be lazy and hard code it. + ''' + url = 'http://www.epubbud.com/search.php?format=atom&q=' + urllib.quote_plus(query) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + # Use html instead of etree as html allows us + # to ignore the namespace easily. + doc = html.fromstring(f.read()) + for data in doc.xpath('//entry'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//id/text()')) + if not id: + continue + + cover_url = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/thumbnail"]/@href')) + + title = u''.join(data.xpath('.//title/text()')) + author = u''.join(data.xpath('.//author/name/text()')) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + s.author = author.strip() + s.price = '$0.00' + s.detail_item = id.strip() + s.drm = SearchResult.DRM_UNLOCKED + s.formats = 'EPUB' + + yield s From 7e2e3cbb87fd5d00e285e90826b5155d2771040c Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 1 Jun 2011 18:25:09 -0400 Subject: [PATCH 17/17] Store: EpubBud store, fix detail url --- src/calibre/gui2/store/epubbud_plugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/calibre/gui2/store/epubbud_plugin.py b/src/calibre/gui2/store/epubbud_plugin.py index 6c20f5150d..d6193f6ae0 100644 --- a/src/calibre/gui2/store/epubbud_plugin.py +++ b/src/calibre/gui2/store/epubbud_plugin.py @@ -24,9 +24,6 @@ class EpubBudStore(BasicStoreConfig, StorePlugin): def open(self, parent=None, detail_item=None, external=False): url = 'http://epubbud.com/' - - if detail_item: - url = 'http://epubbud.com/book.php?g=' + detail_item if external or self.config.get('open_external', False): open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))