diff --git a/recipes/alt_om_herning.recipe b/recipes/alt_om_herning.recipe new file mode 100644 index 0000000000..c60d142a85 --- /dev/null +++ b/recipes/alt_om_herning.recipe @@ -0,0 +1,43 @@ +__license__ = 'GPL v3' +__copyright__ = '2011, Rasmus Lauritsen ' +''' +aoh.dk +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class aoh_dk(BasicNewsRecipe): + title = 'Alt om Herning' + __author__ = 'Rasmus Lauritsen' + description = 'Nyheder fra Herning om omegn' + publisher = 'Mediehuset Herning Folkeblad' + category = 'news, local, Denmark' + oldest_article = 14 + max_articles_per_feed = 50 + no_stylesheets = True + delay = 1 + encoding = 'utf8' + use_embedded_content = False + language = 'da' + extra_css = """ body{font-family: Verdana,Arial,sans-serif } + img{margin-bottom: 0.4em} + .txtContent,.stamp{font-size: small} + """ + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + + feeds = [(u'All news', u'http://aoh.dk/rss.xml')] + + keep_only_tags = [ + dict(name='h1') + ,dict(name='span', attrs={'class':['frontpage_body']}) + ] + + remove_tags = [ + dict(name=['object','link']) + ] diff --git a/recipes/mediapart.recipe b/recipes/mediapart.recipe index 0cf8f21032..4540879f72 100644 --- a/recipes/mediapart.recipe +++ b/recipes/mediapart.recipe @@ -71,7 +71,7 @@ class Mediapart(BasicNewsRecipe): br = BasicNewsRecipe.get_browser() if self.username is not None and self.password is not None: br.open('http://www.mediapart.fr/') - br.select_form(nr=1) + br.select_form(nr=0) br['name'] = self.username br['pass'] = self.password br.submit() diff --git a/recipes/metro_uk.recipe b/recipes/metro_uk.recipe new file mode 100644 index 0000000000..deced5976b --- /dev/null +++ b/recipes/metro_uk.recipe @@ -0,0 +1,29 @@ +from calibre.web.feeds.news import BasicNewsRecipe +class AdvancedUserRecipe1306097511(BasicNewsRecipe): + title = u'Metro UK' + + no_stylesheets = True + oldest_article = 1 + max_articles_per_feed = 200 + + __author__ = 'Dave Asbury' + language = 'en_GB' + simultaneous_downloads= 3 + + masthead_url = 'http://e-edition.metro.co.uk/images/metro_logo.gif' + + keep_only_tags = [ + dict(attrs={'class':['img-cnt figure']}), + dict(attrs={'class':['art-img']}), + dict(name='h1'), + dict(name='h2', attrs={'class':'h2'}), + dict(name='div', attrs={'class':'art-lft'}) + ] + remove_tags = [dict(name='div', attrs={'class':[ 'metroCommentFormWrap', + 'commentForm', 'metroCommentInnerWrap', + 'art-rgt','pluck-app pluck-comm','news m12 clrd clr-l p5t', 'flt-r' ]})] + + feeds = [ + (u'News', u'http://www.metro.co.uk/rss/news/'), (u'Money', u'http://www.metro.co.uk/rss/money/'), (u'Sport', u'http://www.metro.co.uk/rss/sport/'), (u'Film', u'http://www.metro.co.uk/rss/metrolife/film/'), (u'Music', u'http://www.metro.co.uk/rss/metrolife/music/'), (u'TV', u'http://www.metro.co.uk/rss/tv/'), (u'Showbiz', u'http://www.metro.co.uk/rss/showbiz/'), (u'Weird News', u'http://www.metro.co.uk/rss/weird/'), (u'Travel', u'http://www.metro.co.uk/rss/travel/'), (u'Lifestyle', u'http://www.metro.co.uk/rss/lifestyle/'), (u'Books', u'http://www.metro.co.uk/rss/lifestyle/books/'), (u'Food', u'http://www.metro.co.uk/rss/lifestyle/restaurants/')] + + diff --git a/recipes/version2.recipe b/recipes/version2.recipe new file mode 100644 index 0000000000..7dc257246a --- /dev/null +++ b/recipes/version2.recipe @@ -0,0 +1,64 @@ +import re + +__license__ = 'GPL v3' +__copyright__ = '2011, Rasmus Lauritsen ' +''' +version2.dk +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class version2(BasicNewsRecipe): + title = 'Version2.dk' + __author__ = 'Rasmus Lauritsen' + description = 'IT News' + publisher = 'version2.dk' + category = 'news, IT, hardware, software, Denmark' + oldest_article = 14 + max_articles_per_feed = 50 + no_stylesheets = True + remove_empty_feeds = True + use_embedded_content = False + encoding = 'iso-8859-1' + language = 'da' + + extra_css = """ + body {font-family: "Verdana",Times,serif} + .articleauthor{color: #9F9F9F; + font-family: Arial, sans-serif; + font-size: small; + text-transform: uppercase} + .rubric,.dd,h6#credit{color: #CD0021; + font-family: Arial, sans-serif; + font-size: small; + text-transform: uppercase} + .descender:first-letter{display: inline; font-size: xx-large; font-weight: bold} + .dd,h6#credit{color: gray} + .c{display: block} + .caption,h2#articleintro{font-style: italic} + .caption{font-size: small} + """ + + preprocess_regexps = [ (re.compile(r']*>'),lambda match: ''), + (re.compile(r']*article-link-id.*?'), lambda match: '')] + + keep_only_tags = [dict(name='div', attrs={'class':'article'})] + + remove_tags = [ + dict(name='p',attrs={'class':'meta links'}), + dict(name='div',attrs={'class':'float-right'}), + dict(name='span',attrs={'class':'article-link-id'}) + ] + + feeds = [ + (u'Seneste nyheder' , u'http://www.version2.dk/feeds/nyheder') + ,(u'Forretningssoftware' , u'http://www.version2.dk/feeds/forretningssoftware') + ,(u'Internet & styresystemer' , u'http://www.version2.dk/feeds/styresystemer') + ,(u'It-arkitektur' , u'http://www.version2.dk/feeds/it-arkitektur') + ,(u'It-styring & outsourcing' , u'http://www.version2.dk/feeds/it-styring') + ,(u'Job & karriere' , u'http://www.version2.dk/feeds/karriere') + ,(u'Mobil it & tele' , u'http://www.version2.dk/feeds/tele') + ,(u'Server/storage & netværk' , u'http://www.version2.dk/feeds/server-storage') + ,(u'Sikkerhed' , u'http://www.version2.dk/feeds/sikkerhed') + ,(u'Softwareudvikling' , u'http://www.version2.dk/feeds/softwareudvikling') + ] diff --git a/resources/template-functions.json b/resources/template-functions.json index 9fdc1782ea..606b102d41 100644 --- a/resources/template-functions.json +++ b/resources/template-functions.json @@ -1,26 +1,27 @@ { "and": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if not args[i]:\n return ''\n i += 1\n return '1'\n", - "contains": "def evaluate(self, formatter, kwargs, mi, locals,\n val, test, value_if_present, value_if_not):\n if re.search(test, val):\n return value_if_present\n else:\n return value_if_not\n", + "contains": "def evaluate(self, formatter, kwargs, mi, locals,\n val, test, value_if_present, value_if_not):\n if re.search(test, val, flags=re.I):\n return value_if_present\n else:\n return value_if_not\n", "divide": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x / y)\n", "uppercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.upper()\n", "strcat": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n res = ''\n for i in range(0, len(args)):\n res += args[i]\n return res\n", - "in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n for v in l:\n if re.search(pat, v):\n return fv\n return nfv\n", + "in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n if l:\n for v in l:\n if re.search(pat, v, flags=re.I):\n return fv\n return nfv\n", "multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n", "ifempty": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):\n if val:\n return val\n else:\n return value_if_empty\n", "booksize": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.book_size is not None:\n try:\n return str(mi.book_size)\n except:\n pass\n return ''\n", "select": "def evaluate(self, formatter, kwargs, mi, locals, val, key):\n if not val:\n return ''\n vals = [v.strip() for v in val.split(',')]\n for v in vals:\n if v.startswith(key+':'):\n return v[len(key)+1:]\n return ''\n", "strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n", "first_non_empty": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return args[i]\n i += 1\n return ''\n", - "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val)\n", + "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val, flags=re.I)\n", "subtract": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x - y)\n", "list_item": "def evaluate(self, formatter, kwargs, mi, locals, val, index, sep):\n if not val:\n return ''\n index = int(index)\n val = val.split(sep)\n try:\n return val[index]\n except:\n return ''\n", "shorten": "def evaluate(self, formatter, kwargs, mi, locals,\n val, leading, center_string, trailing):\n l = max(0, int(leading))\n t = max(0, int(trailing))\n if len(val) > l + len(center_string) + t:\n return val[0:l] + center_string + ('' if t == 0 else val[-t:])\n else:\n return val\n", "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n", "add": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x + y)\n", - "lookup": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if len(args) == 2: # here for backwards compatibility\n if val:\n return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)\n else:\n return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)\n if (len(args) % 2) != 1:\n raise ValueError(_('lookup requires either 2 or an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)\n if re.search(args[i], val):\n return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)\n i += 2\n", + "lookup": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if len(args) == 2: # here for backwards compatibility\n if val:\n return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)\n else:\n return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)\n if (len(args) % 2) != 1:\n raise ValueError(_('lookup requires either 2 or an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)\n if re.search(args[i], val, flags=re.I):\n return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)\n i += 2\n", "template": "def evaluate(self, formatter, kwargs, mi, locals, template):\n template = template.replace('[[', '{').replace(']]', '}')\n return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)\n", "print": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n print args\n return None\n", "merge_lists": "def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator):\n l1 = [l.strip() for l in list1.split(separator) if l.strip()]\n l2 = [l.strip() for l in list2.split(separator) if l.strip()]\n lcl1 = set([icu_lower(l) for l in l1])\n res = []\n for i in l1:\n res.append(i)\n for i in l2:\n if icu_lower(i) not in lcl1:\n res.append(i)\n return ', '.join(sorted(res, key=sort_key))\n", + "str_in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, str, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n c = [v.strip() for v in str.split(sep) if v.strip()]\n if l:\n for v in l:\n for t in c:\n if strcmp(t, v) == 0:\n return fv\n return nfv\n", "titlecase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return titlecase(val)\n", "subitems": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n items = [v.strip() for v in val.split(',')]\n rv = set()\n for item in items:\n component = item.split('.')\n try:\n if ei == 0:\n rv.add('.'.join(component[si:]))\n else:\n rv.add('.'.join(component[si:ei]))\n except:\n pass\n return ', '.join(sorted(rv, key=sort_key))\n", "sublist": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n val = val.split(sep)\n try:\n if ei == 0:\n return sep.join(val[si:])\n else:\n return sep.join(val[si:ei])\n except:\n return ''\n", @@ -32,9 +33,10 @@ "count": "def evaluate(self, formatter, kwargs, mi, locals, val, sep):\n return unicode(len(val.split(sep)))\n", "lowercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.lower()\n", "substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n", - "assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n", - "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n", "or": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n", + "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val, flags=re.I):\n return args[i+1]\n i += 2\n", + "ondevice": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.ondevice_col:\n return _('Yes')\n return ''\n", + "assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n", "raw_field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return unicode(getattr(mi, name, None))\n", "cmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n x = float(x if x else 0)\n y = float(y if y else 0)\n if x < y:\n return lt\n if x == y:\n return eq\n return gt\n" } \ No newline at end of file diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 3e2201f6a4..69407dcb2e 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -74,7 +74,7 @@ class Metadata(object): Metadata from custom columns should be accessed via the get() method, passing in the lookup name for the column, for example: "#mytags". - Use the :meth:`is_null` method to test if a filed is null. + Use the :meth:`is_null` method to test if a field is null. This object also has functions to format fields into strings. @@ -105,7 +105,7 @@ class Metadata(object): def is_null(self, field): ''' - Return True if the value of filed is null in this object. + Return True if the value of field is null in this object. 'null' means it is unknown or evaluates to False. So a title of _('Unknown') is null or a language of 'und' is null. diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 718f0737b3..43465512e0 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -152,7 +152,8 @@ class DeleteAction(InterfaceAction): if not ids: return fmts = self._get_selected_formats( - '

'+_('Choose formats not to be deleted'), ids) + '

'+_('Choose formats not to be deleted.

Note that ' + 'this will never remove all formats from a book.'), ids) if fmts is None: return for id in ids: @@ -161,9 +162,12 @@ class DeleteAction(InterfaceAction): continue bfmts = set([x.lower() for x in bfmts.split(',')]) rfmts = bfmts - set(fmts) - for fmt in rfmts: - self.gui.library_view.model().db.remove_format(id, fmt, - index_is_id=True, notify=False) + if bfmts - rfmts: + # Do not delete if it will leave the book with no + # formats + for fmt in rfmts: + self.gui.library_view.model().db.remove_format(id, fmt, + index_is_id=True, notify=False) self.gui.library_view.model().refresh_ids(ids) self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(), self.gui.library_view.currentIndex()) diff --git a/src/calibre/gui2/dialogs/select_formats.py b/src/calibre/gui2/dialogs/select_formats.py index 5934c8c0f9..aea56ad196 100644 --- a/src/calibre/gui2/dialogs/select_formats.py +++ b/src/calibre/gui2/dialogs/select_formats.py @@ -44,7 +44,7 @@ class SelectFormats(QDialog): self.setLayout(self._l) self.setWindowTitle(_('Choose formats')) self._m = QLabel(msg) - self._m.setWordWrap = True + self._m.setWordWrap(True) self._l.addWidget(self._m) self.formats = Formats(fmt_list) self.fview = QListView(self) diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py index 3d199b156c..b24328f0ad 100644 --- a/src/calibre/gui2/dialogs/template_line_editor.py +++ b/src/calibre/gui2/dialogs/template_line_editor.py @@ -5,12 +5,17 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QLineEdit, QDialog, QGridLayout, QLabel, QCheckBox, - QDialogButtonBox, QColor, QComboBox, QIcon) +from functools import partial +from collections import defaultdict +from PyQt4.Qt import (Qt, QLineEdit, QDialog, QGridLayout, QLabel, QCheckBox, + QIcon, QDialogButtonBox, QColor, QComboBox, QPushButton) + +from calibre.ebooks.metadata.book.base import composite_formatter from calibre.gui2.dialogs.template_dialog import TemplateDialog from calibre.gui2.complete import MultiCompleteLineEdit from calibre.gui2 import error_dialog +from calibre.utils.icu import sort_key class TemplateLineEditor(QLineEdit): @@ -22,114 +27,235 @@ class TemplateLineEditor(QLineEdit): QLineEdit.__init__(self, parent) self.tags = None self.mi = None + self.txt = None def set_mi(self, mi): self.mi = mi - def set_tags(self, tags): - self.tags = tags + def set_db(self, db): + self.db = db def contextMenuEvent(self, event): menu = self.createStandardContextMenu() menu.addSeparator() + action_clear_field = menu.addAction(_('Remove any template from the box')) + action_clear_field.triggered.connect(self.clear_field) action_open_editor = menu.addAction(_('Open Template Editor')) action_open_editor.triggered.connect(self.open_editor) - if self.tags: - action_tag_wizard = menu.addAction(_('Open Tag Wizard')) - action_tag_wizard.triggered.connect(self.tag_wizard) menu.exec_(event.globalPos()) + def clear_field(self): + self.setText('') + self.txt = None + self.setReadOnly(False) + self.setStyleSheet('TemplateLineEditor { color: black }') + def open_editor(self): - t = TemplateDialog(self, self.text(), self.mi) + if self.txt: + t = TemplateDialog(self, self.txt, self.mi) + else: + t = TemplateDialog(self, self.text(), self.mi) t.setWindowTitle(_('Edit template')) if t.exec_(): + self.txt = None self.setText(t.textbox.toPlainText()) + def enable_wizard_button(self, txt): + if not txt or txt.startswith('program:\n#tag wizard'): + return True + return False + + def setText(self, txt): + txt = unicode(txt) + if txt and txt.startswith('program:\n#tag wizard'): + self.txt = txt + self.setReadOnly(True) + QLineEdit.setText(self, '') + QLineEdit.setText(self, _('Template generated by the wizard')) + self.setStyleSheet('TemplateLineEditor { color: gray }') + else: + QLineEdit.setText(self, txt) + def tag_wizard(self): txt = unicode(self.text()) - if txt and not txt.startswith('program:\n#tag wizard'): + if txt and not self.txt: error_dialog(self, _('Invalid text'), _('The text in the box was not generated by this wizard'), show=True, show_copy_button=False) return - d = TagWizard(self, self.tags, unicode(self.text())) + d = TagWizard(self, self.db, unicode(self.txt), self.mi) if d.exec_(): self.setText(d.template) + def text(self): + if self.txt: + return self.txt + return QLineEdit.text(self) + class TagWizard(QDialog): - def __init__(self, parent, tags, txt): + def __init__(self, parent, db, txt, mi): QDialog.__init__(self, parent) - self.setWindowTitle(_('Tag Wizard')) + self.setWindowTitle(_('Coloring Wizard')) self.setWindowIcon(QIcon(I('wizard.png'))) - self.tags = tags + self.mi = mi + + self.columns = [] + self.completion_values = defaultdict(dict) + for k in db.all_field_keys(): + m = db.metadata_for_field(k) + if m['datatype'] in ('text', 'enumeration', 'series') and \ + m['is_category'] and k not in ('identifiers'): + self.columns.append(k) + if m['is_custom']: + self.completion_values[k]['v'] = db.all_custom(m['label']) + elif k == 'tags': + self.completion_values[k]['v'] = db.all_tags() + elif k == 'formats': + self.completion_values[k]['v'] = db.all_formats() + else: + if k in ('publisher'): + ck = k + 's' + else: + ck = k + f = getattr(db, 'all_' + ck, None) + if f: + if k == 'authors': + self.completion_values[k]['v'] = [v[1].\ + replace('|', ',') for v in f()] + else: + self.completion_values[k]['v'] = [v[1] for v in f()] + + if k in self.completion_values: + if k == 'authors': + self.completion_values[k]['m'] = None + else: + self.completion_values[k]['m'] = m['is_multiple'] + + self.columns.sort(key=sort_key) + self.columns.insert(0, '') + l = QGridLayout() self.setLayout(l) - l.setColumnStretch(0, 1) - l.setColumnMinimumWidth(0, 300) - h = QLabel(_('Tags (see the popup help for more information)')) + l.setColumnStretch(2, 10) + l.setColumnMinimumWidth(3, 300) + + h = QLabel(_('And')) h.setToolTip('

' + - _('You can enter more than one tag per box, separated by commas. ' - 'The comparison ignores letter case.
' - 'A tag value can be a regular expression. Check the box to turn ' + _('Set this box to indicate that the two conditions must both ' + 'be true to return the "color if value found". For example, you ' + 'can check if two tags are present, if the book has a tag ' + 'and a #read custom column is checked, or if a book has ' + 'some tag and has a particular format.')) + l.addWidget(h, 0, 0, 1, 1) + + h = QLabel(_('Column')) + h.setAlignment(Qt.AlignCenter) + l.addWidget(h, 0, 1, 1, 1) + + h = QLabel(_('Not')) + h.setToolTip('

' + + _('Set this box to indicate that the value must not match ' + 'to return the "color if value found". For example, you ' + 'can check if a tag does not exist by entering that tag ' + 'and checking this box. You can check if tags are empty by ' + 'checking this box, entering .* (period asterisk) for the text, ' + 'then checking the RE box. The .* regular expression matches ' + 'anything, so if this box is checked, it matches nothing. ' + 'This box is particularly useful when using the AND box.')) + h.setAlignment(Qt.AlignCenter) + l.addWidget(h, 0, 2, 1, 1) + + h = QLabel(_('Values (see the popup help for more information)')) + h.setAlignment(Qt.AlignCenter) + h.setToolTip('

' + + _('You can enter more than one value per box, separated by commas. ' + 'The comparison ignores letter case. Special note: you can ' + 'enter at most one author.
' + 'A value can be a regular expression. Check the box to turn ' 'them on. When using regular expressions, note that the wizard ' 'puts anchors (^ and $) around the expression, so you ' 'must ensure your expression matches from the beginning ' - 'to the end of the tag.
' + 'to the end of the column you are checking.
' 'Regular expression examples:') + '

    ' + - _('
  • .* matches any tag. No empty tags are ' - 'checked, so you don\'t need to worry about empty strings
  • ' - '
  • A.* matches any tag beginning with A
  • ' - '
  • .*mystery.* matches any tag containing ' + _('
  • .* matches anything in the column. No ' + 'empty values are checked, so you don\'t need to worry about ' + 'empty strings
  • ' + '
  • A.* matches anything beginning with A
  • ' + '
  • .*mystery.* matches anything containing ' 'the word "mystery"
  • ') + '

') - l.addWidget(h , 0, 0, 1, 1) + l.addWidget(h , 0, 3, 1, 1) c = QLabel(_('is RE')) c.setToolTip('

' + - _('Check this box if the tag box contains regular expressions') + '

') - l.addWidget(c, 0, 1, 1, 1) + _('Check this box if the values box contains regular expressions') + '

') + l.addWidget(c, 0, 4, 1, 1) - c = QLabel(_('Color if tag found')) + c = QLabel(_('Color if value found')) c.setToolTip('

' + _('At least one of the two color boxes must have a value. Leave ' 'one color box empty if you want the template to use the next ' 'line in this wizard. If both boxes are filled in, the rest of ' 'the lines in this wizard will be ignored.') + '

') - l.addWidget(c, 0, 2, 1, 1) - c = QLabel(_('Color if tag not found')) + l.addWidget(c, 0, 5, 1, 1) + c = QLabel(_('Color if value not found')) c.setToolTip('

' + _('This box is usually filled in only on the last test. If it is ' - 'filled in before the last test, then the color for tag found box ' + 'filled in before the last test, then the color for value found box ' 'must be empty or all the rest of the tests will be ignored.') + '

') - l.addWidget(c, 0, 3, 1, 1) + l.addWidget(c, 0, 6, 1, 1) + + self.andboxes = [] + self.notboxes = [] self.tagboxes = [] self.colorboxes = [] self.nfcolorboxes = [] self.reboxes = [] + self.colboxes = [] self.colors = [unicode(s) for s in list(QColor.colorNames())] self.colors.insert(0, '') - for i in range(0, 10): + + maxlines = 10 + for i in range(1, maxlines+1): + ab = QCheckBox(self) + self.andboxes.append(ab) + if i != maxlines: + # let the last box float in space + l.addWidget(ab, i, 0, 2, 1) + ab.stateChanged.connect(partial(self.and_box_changed, line=i-1)) + else: + ab.setVisible(False) + + w = QComboBox(self) + w.addItems(self.columns) + l.addWidget(w, i, 1, 1, 1) + self.colboxes.append(w) + + nb = QCheckBox(self) + self.notboxes.append(nb) + l.addWidget(nb, i, 2, 1, 1) + tb = MultiCompleteLineEdit(self) tb.set_separator(', ') - tb.update_items_cache(self.tags) self.tagboxes.append(tb) - l.addWidget(tb, i+1, 0, 1, 1) + l.addWidget(tb, i, 3, 1, 1) + w.currentIndexChanged[str].connect(partial(self.column_changed, valbox=tb)) w = QCheckBox(self) self.reboxes.append(w) - l.addWidget(w, i+1, 1, 1, 1) + l.addWidget(w, i, 4, 1, 1) w = QComboBox(self) w.addItems(self.colors) self.colorboxes.append(w) - l.addWidget(w, i+1, 2, 1, 1) + l.addWidget(w, i, 5, 1, 1) w = QComboBox(self) w.addItems(self.colors) self.nfcolorboxes.append(w) - l.addWidget(w, i+1, 3, 1, 1) + l.addWidget(w, i, 6, 1, 1) if txt: lines = txt.split('\n')[3:] @@ -141,39 +267,105 @@ class TagWizard(QDialog): t, c = vals nc = '' re = False + f = 'tags' + a = False + n = False else: - t,c,nc,re = vals + t,c,f,nc,re,a,n = vals try: - self.colorboxes[i].setCurrentIndex(self.colorboxes[i].findText(c)) - self.nfcolorboxes[i].setCurrentIndex(self.nfcolorboxes[i].findText(nc)) + self.colboxes[i].setCurrentIndex(self.colboxes[i].findText(f)) + self.colorboxes[i].setCurrentIndex( + self.colorboxes[i].findText(c)) + self.nfcolorboxes[i].setCurrentIndex( + self.nfcolorboxes[i].findText(nc)) self.tagboxes[i].setText(t) self.reboxes[i].setChecked(re == '2') + self.andboxes[i].setChecked(a == '2') + self.notboxes[i].setChecked(n == '2') + i += 1 except: pass - i += 1 + + w = QLabel(_('Preview')) + l.addWidget(w, 99, 1, 1, 1) + w = self.test_box = QLineEdit(self) + w.setReadOnly(True) + l.addWidget(w, 99, 3, 1, 1) + w = QPushButton(_('Test')) + l.addWidget(w, 99, 5, 1, 1) + w.clicked.connect(self.preview) bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel, parent=self) - l.addWidget(bb, 100, 2, 1, 2) + l.addWidget(bb, 100, 3, 1, 2) bb.accepted.connect(self.accepted) bb.rejected.connect(self.reject) self.template = '' - def accepted(self): + def preview(self): + if not self.generate_program(): + return + t = composite_formatter.safe_format(self.template, self.mi, + _('EXCEPTION'), self.mi) + self.test_box.setText(t) + + def column_changed(self, s, valbox=None): + k = unicode(s) + 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) + else: + valbox.update_items_cache([]) + valbox.set_separator(None) + + def generate_program(self): res = ("program:\n#tag wizard -- do not directly edit\n" - " t = field('tags');\n first_non_empty(\n") + " first_non_empty(\n") lines = [] - for tb, cb, nfcb, reb in zip(self.tagboxes, self.colorboxes, - self.nfcolorboxes, self.reboxes): - tags = [t.strip() for t in unicode(tb.text()).split(',') if t.strip()] + was_and = False + had_line = False + + line = 0 + for tb, cb, fb, nfcb, reb, ab, nb in zip( + self.tagboxes, self.colorboxes, self.colboxes, + self.nfcolorboxes, self.reboxes, self.andboxes, self.notboxes): + f = unicode(fb.currentText()) + if not f: + continue + m = self.completion_values[f]['m'] c = unicode(cb.currentText()).strip() nfc = unicode(nfcb.currentText()).strip() re = reb.checkState() - if re == 2: - tags = '$|^'.join(tags) + a = ab.checkState() + n = nb.checkState() + line += 1 + + if n == 2: + tval = '' + fval = '1' else: - tags = ','.join(tags) - if not tags or not (c or nfc): - continue + tval = '1' + fval = '' + + if m: + tags = [t.strip() for t in unicode(tb.text()).split(',') if t.strip()] + if re == 2: + tags = '$|^'.join(tags) + else: + tags = ','.join(tags) + else: + tags = unicode(tb.text()).strip() + if f == 'authors': + tags.replace(',', '|') + + if (tags or f) and not (tags and f and (a == 2 or c)): + error_dialog(self, _('Invalid line'), + _('Line number {0} is not valid').format(line), + show=True, show_copy_button=False) + return False + if c not in self.colors: error_dialog(self, _('Invalid color'), _('The color {0} is not valid').format(c), @@ -184,25 +376,69 @@ class TagWizard(QDialog): _('The color {0} is not valid').format(nfc), show=True, show_copy_button=False) return False - if re == 2: - lines.append(" in_list(t, ',', '^{0}$', '{1}', '{2}')".\ - format(tags, c, nfc)) + + if not was_and: + if had_line: + lines[-1] += ',' + had_line = True + lines.append(" test(and(") else: - lines.append(" str_in_list(t, ',', '{0}', '{1}', '{2}')".\ - format(tags, c, nfc)) - res += ',\n'.join(lines) + lines[-1] += ',' + + if re == 2: + if m: + lines.append(" in_list(field('{1}'), ',', '^{0}$', '{2}', '{3}')".\ + format(tags, f, tval, fval)) + else: + lines.append(" contains(field('{1}'), '{0}', '{2}', '{3}')".\ + format(tags, f, tval, fval)) + else: + if m: + lines.append(" str_in_list(field('{1}'), ',', '{0}', '{2}', '{3}')".\ + format(tags, f, tval, fval)) + else: + lines.append(" strcmp(field('{1}'), '{0}', '{3}', '{2}', '{3}')".\ + format(tags, f, tval, fval)) + if a == 2: + was_and = True + else: + was_and = False + lines.append(" ), '{0}', '{1}')".format(c, nfc)) + + res += '\n'.join(lines) res += ')\n' self.template = res res = '' - for tb, cb, nfcb, reb in zip(self.tagboxes, self.colorboxes, - self.nfcolorboxes, self.reboxes): + for tb, cb, fb, nfcb, reb, ab, nb in zip( + self.tagboxes, self.colorboxes, self.colboxes, + self.nfcolorboxes, self.reboxes, self.andboxes, self.notboxes): t = unicode(tb.text()).strip() if t.endswith(','): t = t[:-1] c = unicode(cb.currentText()).strip() + f = unicode(fb.currentText()) nfc = unicode(nfcb.currentText()).strip() re = unicode(reb.checkState()) - if t and c: - res += '#' + t + ':|:' + c + ':|:' + nfc + ':|:' + re + '\n' + a = unicode(ab.checkState()) + n = unicode(nb.checkState()) + if f and t and (a == '2' or c): + res += '#' + t + ':|:' + c + ':|:' + f + ':|:' + \ + nfc + ':|:' + re + ':|:' + a + ':|:' + n + '\n' self.template += res - self.accept() + return True + + def and_box_changed(self, state, line=None): + if state == 2: + self.colorboxes[line].setCurrentIndex(0) + self.colorboxes[line].setEnabled(False) + self.nfcolorboxes[line].setCurrentIndex(0) + self.nfcolorboxes[line].setEnabled(False) + else: + self.colorboxes[line].setEnabled(True) + self.nfcolorboxes[line].setEnabled(True) + + def accepted(self): + if self.generate_program(): + self.accept() + else: + self.template = '' diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 554b104c34..d79c92befa 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en' import shutil, functools, re, os, traceback from contextlib import closing +from collections import defaultdict from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, QModelIndex, QVariant, QDate, QColor) @@ -87,6 +88,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.column_map = [] self.headers = {} self.alignment_map = {} + self.color_cache = defaultdict(dict) self.buffer_size = buffer self.metadata_backup = None self.bool_yes_icon = QIcon(I('ok.png')) @@ -97,7 +99,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.ids_to_highlight_set = set() self.current_highlighted_idx = None self.highlight_only = False - self.column_color_map = {} + self.column_color_list = [] self.colors = [unicode(c) for c in QColor.colorNames()] self.read_config() @@ -172,11 +174,13 @@ class BooksModel(QAbstractTableModel): # {{{ def refresh_ids(self, ids, current_row=-1): + self.color_cache = defaultdict(dict) rows = self.db.refresh_ids(ids) if rows: self.refresh_rows(rows, current_row=current_row) def refresh_rows(self, rows, current_row=-1): + self.color_cache = defaultdict(dict) for row in rows: if row == current_row: self.new_bookdisplay_data.emit( @@ -206,6 +210,7 @@ class BooksModel(QAbstractTableModel): # {{{ return ret def count_changed(self, *args): + self.color_cache = defaultdict(dict) self.count_changed_signal.emit(self.db.count()) def row_indices(self, index): @@ -336,6 +341,10 @@ class BooksModel(QAbstractTableModel): # {{{ self.db.refresh(field=None) self.resort(reset=reset) + def reset(self): + self.color_cache = defaultdict(dict) + QAbstractTableModel.reset(self) + def resort(self, reset=True): if not self.db: return @@ -537,12 +546,12 @@ class BooksModel(QAbstractTableModel): # {{{ return img def set_color_templates(self, reset=True): - self.column_color_map = {} + self.column_color_list = [] for i in range(1,self.db.column_color_count+1): name = self.db.prefs.get('column_color_name_'+str(i)) if name: - self.column_color_map[name] = \ - self.db.prefs.get('column_color_template_'+str(i)) + self.column_color_list.append((name, + self.db.prefs.get('column_color_template_'+str(i)))) if reset: self.reset() @@ -717,18 +726,25 @@ class BooksModel(QAbstractTableModel): # {{{ return QVariant(QColor('lightgreen')) elif role == Qt.ForegroundRole: key = self.column_map[col] - if key in self.column_color_map: + for k,fmt in self.column_color_list: + if k != key: + continue + id_ = self.id(index) + if id_ in self.color_cache: + if key in self.color_cache[id_]: + return self.color_cache[id_][key] mi = self.db.get_metadata(self.id(index), index_is_id=True) - fmt = self.column_color_map[key] try: color = composite_formatter.safe_format(fmt, mi, '', mi) if color in self.colors: color = QColor(color) if color.isValid(): - return QVariant(color) + color = QVariant(color) + self.color_cache[id_][key] = color + return color except: return NONE - elif self.is_custom_column(key) and \ + if self.is_custom_column(key) and \ self.custom_columns[key]['datatype'] == 'enumeration': cc = self.custom_columns[self.column_map[col]]['display'] colors = cc.get('enum_colors', []) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 862636cb04..7a8c1fb69c 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -5,12 +5,15 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +from functools import partial + from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, - QAbstractListModel, Qt, QColor) + QAbstractListModel, Qt, QColor, QIcon, QToolButton, QComboBox) from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences.look_feel_ui import Ui_Form from calibre.gui2 import config, gprefs, qt_app +from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor from calibre.utils.localization import (available_translations, get_language, get_lang) from calibre.utils.config import prefs @@ -167,14 +170,15 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): '' 'tutorial on using templates.') + '

' + - _('If you want to color a field based on tags, then click the ' - 'button next to an empty line to open the tags wizard. ' + _('If you want to color a field based on contents of columns, ' + 'then click the button next to an empty line to open the wizard. ' 'It will build a template for you. You can later edit that ' - 'template with the same wizard. If you edit it by hand, the ' - 'wizard might not work or might restore old values.') + + 'template with the same wizard. This is by far the easiest ' + 'way to specify a template.') + '

' + - _('The template must evaluate to one of the color names shown ' - 'below. You can use any legal template expression. ' + _('If you manually construct a template, then the template must ' + 'evaluate to a valid color name shown in the color names box.' + 'You can use any legal template expression. ' 'For example, you can set the title to always display in ' 'green using the template "green" (without the quotes). ' 'To show the title in the color named in the custom column ' @@ -200,11 +204,16 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): 'of values", it is often easier to specify the ' 'colors in the column definition dialog. There you can ' 'provide a color for each value without using a template.')+ '

') + self.color_help_scrollArea.setVisible(False) + self.color_help_button.clicked.connect(self.change_help_text) + self.colors_scrollArea.setVisible(False) + self.colors_label.setVisible(False) + self.colors_button.clicked.connect(self.change_colors_text) + choices = db.field_metadata.displayable_field_keys() choices.sort(key=sort_key) choices.insert(0, '') self.column_color_count = db.column_color_count+1 - tags = db.all_tags() mi=None try: @@ -213,17 +222,58 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): except: pass + l = self.column_color_layout for i in range(1, self.column_color_count): + ccn = QComboBox(parent=self) + setattr(self, 'opt_column_color_name_'+str(i), ccn) + l.addWidget(ccn, i, 0, 1, 1) + + wtb = QToolButton(parent=self) + setattr(self, 'opt_column_color_wizard_'+str(i), wtb) + wtb.setIcon(QIcon(I('wizard.png'))) + l.addWidget(wtb, i, 1, 1, 1) + + ttb = QToolButton(parent=self) + setattr(self, 'opt_column_color_tpledit_'+str(i), ttb) + ttb.setIcon(QIcon(I('edit_input.png'))) + l.addWidget(ttb, i, 2, 1, 1) + + tpl = TemplateLineEditor(parent=self) + setattr(self, 'opt_column_color_template_'+str(i), tpl) + tpl.textChanged.connect(partial(self.tpl_edit_text_changed, ctrl=i)) + tpl.set_db(db) + tpl.set_mi(mi) + l.addWidget(tpl, i, 3, 1, 1) + + wtb.clicked.connect(tpl.tag_wizard) + ttb.clicked.connect(tpl.open_editor) + r('column_color_name_'+str(i), db.prefs, choices=choices) r('column_color_template_'+str(i), db.prefs) - tpl = getattr(self, 'opt_column_color_template_'+str(i)) - tpl.set_tags(tags) - tpl.set_mi(mi) - toolbutton = getattr(self, 'opt_column_color_wizard_'+str(i)) - toolbutton.clicked.connect(tpl.tag_wizard) + txt = db.prefs.get('column_color_template_'+str(i), None) + + wtb.setEnabled(tpl.enable_wizard_button(txt)) + ttb.setEnabled(not tpl.enable_wizard_button(txt) or not txt) + all_colors = [unicode(s) for s in list(QColor.colorNames())] self.colors_box.setText(', '.join(all_colors)) + def change_help_text(self): + self.color_help_scrollArea.setVisible(not self.color_help_scrollArea.isVisible()) + + def change_colors_text(self): + self.colors_scrollArea.setVisible(not self.colors_scrollArea.isVisible()) + self.colors_label.setVisible(not self.colors_label.isVisible()) + + def tpl_edit_text_changed(self, ign, ctrl=None): + tpl = getattr(self, 'opt_column_color_template_'+str(ctrl)) + txt = unicode(tpl.text()) + wtb = getattr(self, 'opt_column_color_wizard_'+str(ctrl)) + ttb = getattr(self, 'opt_column_color_tpledit_'+str(ctrl)) + wtb.setEnabled(tpl.enable_wizard_button(txt)) + ttb.setEnabled(not tpl.enable_wizard_button(txt) or not txt) + tpl.setFocus() + def initialize(self): ConfigWidgetBase.initialize(self) font = gprefs['font'] diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index fe6134f235..def1bdd41c 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -416,114 +416,95 @@ then the tags will be displayed each on their own line. Column Coloring - + Column to color - - - - Color selection template - - - - - - - - - - - - - - :/images/wizard.png:/images/wizard.png - - - Open the tags wizard. - - - - - - - - - - - - - - :/images/wizard.png:/images/wizard.png - - - Open the tags wizard. - - - - - - - - - - - - - - :/images/wizard.png:/images/wizard.png - - - Open the tags wizard. - - - - - - - - - - - - - - :/images/wizard.png:/images/wizard.png - - - Open the tags wizard. - - - - - - - - - - - - - - :/images/wizard.png:/images/wizard.png - - - Open the tags wizard. - - + + + + + + Color selection template + + + + 10 + 0 + + + + + + + + The template wizard is easiest to use + + + + + + + Show/hide help text + + + + + + + Show/hide colors + + + + - + Color names - - + + + + + 16777215 + 300 + + + + true + + + + + 0 + 0 + 687 + 61 + + + + + + + true + + + Qt::AlignLeft|Qt::AlignTop + + + + + + + + + 0 @@ -534,7 +515,7 @@ then the tags will be displayed each on their own line. true - Qt::AlignCenter + Qt::AlignLeft|Qt::AlignTop @@ -560,37 +541,24 @@ then the tags will be displayed each on their own line. - - - + + + + + 0 + 10 + + + + Qt::Vertical + + - 16777215 - 120 + 0 + 0 - - true - - - - - 0 - 0 - 687 - 61 - - - - - - - true - - - - - - + diff --git a/src/calibre/manual/gui.rst b/src/calibre/manual/gui.rst index a4e18c2e07..e2758bc257 100644 --- a/src/calibre/manual/gui.rst +++ b/src/calibre/manual/gui.rst @@ -352,7 +352,7 @@ The syntax for searching for dates is:: If the date is ambiguous, the current locale is used for date comparison. For example, in an mm/dd/yyyy locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009. Some special date strings are available. The string ``today`` translates to today's date, whatever it is. The -strings `yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare +strings ``yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare to a date some number of days ago, for example: date:>10daysago, date:<=45daysago. You can search for books that have a format of a certain size like this:: diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index f1d2844d37..ef44b0a5c9 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -122,7 +122,7 @@ The functions available are: * ``uppercase()`` -- return the value of the field in upper case. * ``titlecase()`` -- return the value of the field in title case. * ``capitalize()`` -- return the value with the first letter upper case and the rest lower case. - * ``contains(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 `text if no match`. + * ``contains(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 `text if no match`. * ``count(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 uses an ampersand. Examples: `{tags:count(,)}`, `{authors:count(&)}` * ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`. * ``in_list(separator, pattern, found_val, not_found_val)`` -- interpret the field as a list of items separated by `separator`, comparing the `pattern` against each value in the list. If the pattern matches a value, return `found_val`, otherwise return `not_found_val`. diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 32822e1d72..b66aec2cb9 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -331,9 +331,10 @@ class BuiltinInList(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv): l = [v.strip() for v in val.split(sep) if v.strip()] - for v in l: - if re.search(pat, v, flags=re.I): - return fv + if l: + for v in l: + if re.search(pat, v, flags=re.I): + return fv return nfv class BuiltinStrInList(BuiltinFormatterFunction): @@ -349,10 +350,11 @@ class BuiltinStrInList(BuiltinFormatterFunction): def evaluate(self, formatter, kwargs, mi, locals, val, sep, str, fv, nfv): l = [v.strip() for v in val.split(sep) if v.strip()] c = [v.strip() for v in str.split(sep) if v.strip()] - for v in l: - for t in c: - if strcmp(t, v) == 0: - return fv + if l: + for v in l: + for t in c: + if strcmp(t, v) == 0: + return fv return nfv class BuiltinRe(BuiltinFormatterFunction): diff --git a/src/calibre/utils/zipfile.py b/src/calibre/utils/zipfile.py index e0f453452c..fa15bff4b4 100644 --- a/src/calibre/utils/zipfile.py +++ b/src/calibre/utils/zipfile.py @@ -1123,6 +1123,13 @@ class ZipFile: targetpath = os.sep.join(components) with open(targetpath, 'wb') as target: shutil.copyfileobj(source, target) + # Kovid: Try to preserve the timestamps in the ZIP file + try: + mtime = time.localtime() + mtime = time.mktime(member.date_time + (0, 0) + (mtime.tm_isdst,)) + os.utime(targetpath, (mtime, mtime)) + except: + pass self.extract_mapping[member.filename] = targetpath return targetpath