Merge from trunk

This commit is contained in:
Sengian 2010-12-03 19:38:48 +01:00
commit 9cc182c669
64 changed files with 10442 additions and 7717 deletions

View File

@ -4,6 +4,107 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.32
date: 2010-12-03
new features:
- title: "All new linux binary build. With updated libraries and replacing cx_Freeze with my own C python launcher code."
- title: "Edit metadata dialog: Add Next and Previous buttons and show cover size in tooltip"
tickets: [7706, 7711]
- title: "A new custom column type: Enumeration. This column can take one of a user defined set of values."
- title: "PML Output: Add option to reduce image sizes/bit depth to allow PML Output to be used with DropBook"
- title: "TXT Output: Add option to generate Markdown output. Turn <br> tags into spaces."
- title: "Add a count function to the template language. Make author_sort searchable."
- title: "Various consistency and usability enhancements to the search box."
tickets: [7726]
description: >
"Always select first book in result set of a search. Similar books searches added to search history. Search history order is no longer randomized. When focussing the search box with a keyboard shortcut, select all text. If you press enter in the search box, the search is executed and the book list os automatically focussed."
- title: "Driver for samsung fascinate and PocketBook 902"
- title: "FB2 Output: Add option to create FB2 sections based on internal file structure of input file (useful for EPUB files that have been split on chapter boundaries). Also add options to mark h1/h2/h3 tags as section titles in the FB2 file."
tickets: [7738]
- title: "Metadata jacket: Add publisher information to jacket."
- title: "Catalog generation: Allow use of custom columns as well as tags to indicate read books. Note that your previously saved read books setting will be lost."
- title: "Bulk metadata edit dialog: Add an Apply button to allow you to perform multiple operations in sequence"
- title: "Allow drag and drop of books onto user categories. If you drag a book from a particular column (say authors) and drop it onto a user category, the column value will be added to the user category. So for authors, the authros will be added to the user category."
- title: "Check Library can now check and repair the has_cover cache"
- title: "Allow GUI plugins to be distributed in ZIP files. See http://www.mobileread.com/forums/showthread.php?t=108774"
- title: "Allow searching by the number of tags/authors/formats/etc. See User Manual for details."
- title: "Tiny speed up when loading large libraries and make various metadata editing tasks a little faster by reducing the number of times the Tag Browser is refreshed"
bug fixes:
- title: "E-book viewer: Fix broken backwards searching"
- title: "Fix custom ratings column values being displayed incorrectly in book details area"
tickets: [7740]
- title: "Fix book details dialog not using internal viewer to view ebooks"
tickets: [7424]
- title: "MOBI Output: When the input document does not explicitly specify a size for images, set the size to be the natural size of the image. This works around Amazon's *truly wonderful* MOBI renderer's tendency to expand images that do not have a width and height specified."
- title: "Conversion pipeline: Fix bug that caused height/width specified in %/em of screen size to be incorrectly calculated by a factor of 72./DPI"
- title: "Conversion pipeline: Respect max-width and max-height when calculating the effective size of an element"
- title: "Conversion pipeline: Do not override CSS for images with the value of the img width/height attributes, unless no CSS is specified for the image"
- title: "E-book viewer: Resize automatically to fit on smaller screens"
- title: "Use the same MIME database on all platforms that calibre runs on, works around python 2.7's crazy insistence on reading MIME data from the registry"
- title: "Kobo driver: Allow html, txt and rtf documents to be deleted"
- title: "Always overwrite title/author metadata when downloading metadata for books added by ISBN"
- title: "Nook Color profile: Reduce screen height to 900px"
- title: "Fix regression that broke RTF conversion on some linux systems"
- title: "Fix bug that could break searching after copying and deleting a book from the current library"
tickets: [7459]
improved recipes:
- NZZ
- Frankfurter Rundschau
- JiJi Press
- Revista Muy Intersante
new recipes:
- title: "Global Times"
author: "malfi"
- title: "The Philosopher's Magazine"
author: "Darko Miletic"
- title: "Poughkeepsie Journal"
author: "weebl"
- title: "Business Spectator and ABC Australia"
author: "Dean Cording"
- title: "La Rijoa and NacionRed"
author: "Arturo Martinez Nieves"
- title: "Animal Politico"
author: "leamsi"
- version: 0.7.31
date: 2010-11-27

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

View File

@ -0,0 +1,46 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class globaltimes(BasicNewsRecipe):
title = u'Global Times'
__author__ = 'malfi'
language = 'zh'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
cover_url = 'http://enhimg2.huanqiu.com/images/logo.png'
language = 'en'
keep_only_tags = []
keep_only_tags.append(dict(name = 'div', attrs = {'id': 'content'}))
remove_tags = []
remove_tags.append(dict(name = 'div', attrs = {'class': 'location'}))
remove_tags.append(dict(name = 'div', attrs = {'class': 'contentpage'}))
remove_tags.append(dict(name = 'li', attrs = {'id': 'pl'}))
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
def parse_index(self):
catnames = {}
catnames["http://china.globaltimes.cn/chinanews/"] = "China Politics"
catnames["http://china.globaltimes.cn/diplomacy/"] = "China Diplomacy"
catnames["http://military.globaltimes.cn/china/"] = "China Military"
catnames["http://business.globaltimes.cn/china-economy/"] = "China Economy"
catnames["http://world.globaltimes.cn/asia-pacific/"] = "Asia Pacific"
feeds = []
for cat in catnames.keys():
articles = []
soup = self.index_to_soup(cat)
for a in soup.findAll('a',attrs={'href' : re.compile(cat+"201[0-9]-[0-1][0-9]/[0-9][0-9][0-9][0-9][0-9][0-9].html")}):
url = a['href'].strip()
myarticle=({'title':self.tag_to_string(a), 'url':url, 'description':'', 'date':''})
self.log("found %s" % url)
articles.append(myarticle)
self.log("Adding URL %s\n" %url)
if articles:
feeds.append((catnames[cat], articles))
return feeds

View File

@ -8,8 +8,8 @@ www.nin.co.rs
import re
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
from contextlib import nested, closing
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString, CData, Tag
from contextlib import closing
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre import entity_to_unicode
class Nin(BasicNewsRecipe):

View File

@ -282,9 +282,9 @@ class NYTimes(BasicNewsRecipe):
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('http://www.nytimes.com/auth/login')
br.select_form(name='login')
br['USERID'] = self.username
br['PASSWORD'] = self.password
br.form = br.forms().next()
br['userid'] = self.username
br['password'] = self.password
raw = br.submit().read()
if 'Please try again' in raw:
raise Exception('Your username and password are incorrect')

View File

@ -282,9 +282,9 @@ class NYTimes(BasicNewsRecipe):
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('http://www.nytimes.com/auth/login')
br.select_form(name='login')
br['USERID'] = self.username
br['PASSWORD'] = self.password
br.form = br.forms().next()
br['userid'] = self.username
br['password'] = self.password
raw = br.submit().read()
if 'Please try again' in raw:
raise Exception('Your username and password are incorrect')

View File

@ -1,6 +1,6 @@
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.nzz.ch
@ -20,6 +20,19 @@ class Nzz(BasicNewsRecipe):
encoding = 'utf-8'
use_embedded_content = False
language = 'de'
extra_css = """
body{font-family: Georgia,"Times New Roman",Times,serif }
.artikel h3,.artikel h4,.bildLegende,.question,.autor{font-family: Arial,Verdana,Helvetica,sans-serif}
.bildLegende{font-size: small}
.autor{font-size: 0.9375em; color: #666666}
.quote{font-size: large !important;
font-style: italic;
font-weight: normal !important;
border-bottom: 1px dotted #BFBFBF;
border-top: 1px dotted #BFBFBF;
line-height: 1.25em}
.quelle{color: #666666; font-style: italic; white-space: nowrap}
"""
conversion_options = {
'comments' : description
@ -28,12 +41,14 @@ class Nzz(BasicNewsRecipe):
,'publisher' : publisher
}
keep_only_tags = [dict(name='div', attrs={'class':'article'})]
keep_only_tags = [dict(name='div', attrs={'class':'zone'})]
remove_tags_before = dict(name='p', attrs={'class':'dachzeile'})
remove_tags_after=dict(name='p', attrs={'class':'fussnote'})
remove_attributes=['width','height','lang']
remove_tags = [
dict(name=['object','link','base'])
,dict(name='div',attrs={'class':['more','teaser','advXertXoriXals','legal']})
,dict(name='div',attrs={'id':['popup-src','readercomments','google-ad','advXertXoriXals']})
dict(name=['object','link','base','meta','iframe'])
,dict(attrs={'id':'content_rectangle_1'})
,dict(attrs={'class':['weiterfuehrendeLinks','fussnote','video']})
]
feeds = [
@ -50,7 +65,7 @@ class Nzz(BasicNewsRecipe):
,(u'Reisen' , u'http://www.nzz.ch/magazin/reisen?rss=true')
]
def print_version(self, url):
return url + '?printview=true'
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)

View File

@ -0,0 +1,19 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1291143841(BasicNewsRecipe):
title = u'Poughkeepsipe Journal'
language = 'en'
__author__ = 'weebl'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
timefmt = ' [%a, %d %b, %Y]'
feeds = [(u'Local News', u'http://poughkeepsiejournal.com/apps/pbcs.dll/oversikt?Category=RSS01&mime=xml'),
(u'Local Business', u'http://poughkeepsiejournal.com/apps/pbcs.dll/oversikt?Category=RSS02&mime=xml'),
(u'Local Sports', u'http://poughkeepsiejournal.com/apps/pbcs.dll/oversikt?Category=RSS03&mime=xml'),
(u'Life', u'http://poughkeepsiejournal.com/apps/pbcs.dll/oversikt?Category=RSS04&mime=xml')]
remove_tags = [dict(name='img', attrs={'src':'/graphics/mastlogo.gif'})]
def print_version(self, url):
return url.replace('http://www.poughkeepsiejournal.com', 'http://www.poughkeepsiejournal.com/print')

View File

@ -0,0 +1,72 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.philosophypress.co.uk
'''
from calibre.web.feeds.news import BasicNewsRecipe
class TPM_uk(BasicNewsRecipe):
title = "The Philosophers' Magazine"
__author__ = 'Darko Miletic'
description = 'Title says it all'
publisher = "The Philosophers' Magazine"
category = 'philosophy, news'
oldest_article = 25
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = False
language = 'en_GB'
remove_empty_feeds = True
publication_type = 'magazine'
masthead_url = 'http://www.philosophypress.co.uk/wp-content/themes/masterplan/tma/images/bg/sitelogo.png'
extra_css = """
body{font-family: Helvetica,Arial,"Lucida Grande",Verdana,sans-serif }
img{margin-bottom: 0.4em; display:block}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [
dict(name=['meta','link','base','iframe','embed','object','img'])
,dict(attrs={'id':['respond','sharethis_0']})
,dict(attrs={'class':'wp-caption-text'})
]
keep_only_tags=[
dict(attrs={'class':['post_cat','post_name','post_meta','post_text']})
,dict(attrs={'id':'comments'})
]
remove_attributes=['lang','width','height']
feeds = [
(u'Columns' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=15' )
,(u'Essays' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=19' )
,(u"21'st Century" , u'http://www.philosophypress.co.uk/?feed=rss2&cat=101')
,(u'Interviews' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=9' )
,(u'News' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=28' )
,(u'Profiles' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=59' )
,(u'Reviews' , u'http://www.philosophypress.co.uk/?feed=rss2&cat=12' )
]
def get_cover_url(self):
soup = self.index_to_soup('http://www.philosophypress.co.uk/')
for image in soup.findAll('img',title=True):
if image['title'].startswith('Click to Subscribe'):
return image['src']
return None
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for alink in soup.findAll('a', rel=True):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.31'
__version__ = '0.7.32'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re

View File

@ -37,6 +37,8 @@ class Plugin(_Plugin):
self.fsizes.append((name, num, float(size)))
self.fnames = dict((name, sz) for name, _, sz in self.fsizes if name)
self.fnums = dict((num, sz) for _, num, sz in self.fsizes if num)
self.width_pts = self.width * 72./self.dpi
self.height_pts = self.height * 72./self.dpi
# Input profiles {{{
class InputProfile(Plugin):

View File

@ -531,6 +531,8 @@ class Metadata(object):
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool':
res = _('Yes') if res else _('No')
elif datatype == 'rating':
res = res/2
return (name, unicode(res), orig_res, cmeta)
# Translate aliases into the standard field name

View File

@ -10,9 +10,10 @@ import copy
import re
from lxml import etree
from calibre.ebooks.oeb.base import namespace, barename
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, OEB_DOCS
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, OEB_DOCS, urlnormalize
from calibre.ebooks.oeb.stylizer import Stylizer
from calibre.ebooks.oeb.transforms.flatcss import KeyMapper
from calibre.utils.magick.draw import identify_data
MBP_NS = 'http://mobipocket.com/ns/mbp'
def MBP(name): return '{%s}%s' % (MBP_NS, name)
@ -121,6 +122,7 @@ class MobiMLizer(object):
body = item.data.find(XHTML('body'))
nroot = etree.Element(XHTML('html'), nsmap=MOBI_NSMAP)
nbody = etree.SubElement(nroot, XHTML('body'))
self.current_spine_item = item
self.mobimlize_elem(body, stylizer, BlockState(nbody),
[FormatState()])
item.data = nroot
@ -357,8 +359,9 @@ class MobiMLizer(object):
if tag == 'img' and 'src' in elem.attrib:
istate.attrib['src'] = elem.attrib['src']
istate.attrib['align'] = 'baseline'
cssdict = style.cssdict()
for prop in ('width', 'height'):
if style[prop] != 'auto':
if cssdict[prop] != 'auto':
value = style[prop]
if value == getattr(self.profile, prop):
result = '100%'
@ -371,8 +374,40 @@ class MobiMLizer(object):
(72./self.profile.dpi)))
except:
continue
result = "%d"%pixs
result = str(pixs)
istate.attrib[prop] = result
if 'width' not in istate.attrib or 'height' not in istate.attrib:
href = self.current_spine_item.abshref(elem.attrib['src'])
try:
item = self.oeb.manifest.hrefs[urlnormalize(href)]
except:
self.oeb.logger.warn('Failed to find image:',
href)
else:
try:
width, height = identify_data(item.data)[:2]
except:
self.oeb.logger.warn('Invalid image:', href)
else:
if 'width' not in istate.attrib and 'height' not in \
istate.attrib:
istate.attrib['width'] = str(width)
istate.attrib['height'] = str(height)
else:
ar = float(width)/float(height)
if 'width' not in istate.attrib:
try:
width = int(istate.attrib['height'])*ar
except:
pass
istate.attrib['width'] = str(int(width))
else:
try:
height = int(istate.attrib['width'])/ar
except:
pass
istate.attrib['height'] = str(int(height))
item.unload_data_from_memory()
elif tag == 'hr' and asfloat(style['width']) > 0:
prop = style['width'] / self.profile.width
istate.attrib['width'] = "%d%%" % int(round(prop * 100))

View File

@ -96,7 +96,10 @@ class EbookIterator(object):
def search(self, text, index, backwards=False):
text = text.lower()
for i, path in enumerate(self.spine):
pmap = [(i, path) for i, path in enumerate(self.spine)]
if backwards:
pmap.reverse()
for i, path in pmap:
if (backwards and i < index) or (not backwards and i > index):
if text in open(path, 'rb').read().decode(path.encoding).lower():
return i

View File

@ -253,7 +253,10 @@ class Stylizer(object):
upd = {}
for prop in ('width', 'height'):
val = elem.get(prop, '').strip()
del elem.attrib[prop]
try:
del elem.attrib[prop]
except:
pass
if val:
if num_pat.match(val) is not None:
val += 'px'
@ -572,7 +575,7 @@ class Style(object):
if parent is not None:
base = parent.width
else:
base = self._profile.width
base = self._profile.width_pts
if 'width' in self._element.attrib:
width = self._element.attrib['width']
elif 'width' in self._style:
@ -584,6 +587,13 @@ class Style(object):
if isinstance(result, (unicode, str, bytes)):
result = self._profile.width
self._width = result
if 'max-width' in self._style:
result = self._unit_convert(self._style['max-width'], base=base)
if isinstance(result, (unicode, str, bytes)):
result = self._width
if result < self._width:
self._width = result
return self._width
@property
@ -595,7 +605,7 @@ class Style(object):
if parent is not None:
base = parent.height
else:
base = self._profile.height
base = self._profile.height_pts
if 'height' in self._element.attrib:
height = self._element.attrib['height']
elif 'height' in self._style:
@ -607,6 +617,13 @@ class Style(object):
if isinstance(result, (unicode, str, bytes)):
result = self._profile.height
self._height = result
if 'max-height' in self._style:
result = self._unit_convert(self._style['max-height'], base=base)
if isinstance(result, (unicode, str, bytes)):
result = self._height
if result < self._height:
self._height = result
return self._height
@property

View File

@ -35,6 +35,12 @@ class PMLOutput(OutputFormatPlugin):
OptionRecommendation(name='inline_toc',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Add Table of Contents to beginning of the book.')),
OptionRecommendation(name='full_image_depth',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Do not reduce the size or bit depth of images. Images ' \
'have their size and depth reduced by default to accommodate ' \
'applications that can not convert images on their ' \
'own such as Dropbook.')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
@ -44,16 +50,20 @@ class PMLOutput(OutputFormatPlugin):
with open(os.path.join(tdir, 'index.pml'), 'wb') as out:
out.write(pml.encode(opts.output_encoding, 'replace'))
self.write_images(oeb_book.manifest, pmlmlizer.image_hrefs, tdir)
self.write_images(oeb_book.manifest, pmlmlizer.image_hrefs, tdir, opts)
log.debug('Compressing output...')
pmlz = ZipFile(output_path, 'w')
pmlz.add_dir(tdir)
def write_images(self, manifest, image_hrefs, out_dir):
def write_images(self, manifest, image_hrefs, out_dir, opts):
for item in manifest:
if item.media_type in OEB_RASTER_IMAGES and item.href in image_hrefs.keys():
im = Image.open(cStringIO.StringIO(item.data))
if opts.full_image_depth:
im = Image.open(cStringIO.StringIO(item.data))
else:
im = Image.open(cStringIO.StringIO(item.data)).convert('P')
im.thumbnail((300,300), Image.ANTIALIAS)
data = cStringIO.StringIO()
im.save(data, 'PNG')

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
'''
Transform OEB content into Markdown formatted plain text
'''
import re
from lxml import etree
from calibre.utils.html2text import html2text
class MarkdownMLizer(object):
def __init__(self, log):
self.log = log
def extract_content(self, oeb_book, opts):
self.log.info('Converting XHTML to Markdown formatted TXT...')
self.oeb_book = oeb_book
self.opts = opts
return self.mlize_spine()
def mlize_spine(self):
output = [u'']
for item in self.oeb_book.spine:
self.log.debug('Converting %s to Markdown formatted TXT...' % item.href)
html = unicode(etree.tostring(item.data, encoding=unicode))
if not self.opts.keep_links:
html = re.sub(r'<\s*a[^>]*>', '', html)
html = re.sub(r'<\s*/\s*a\s*>', '', html)
if not self.opts.keep_image_references:
html = re.sub(r'<\s*img[^>]*>', '', html)
html = re.sub(r'<\s*img\s*>', '', html)
text = html2text(html)
# Ensure the section ends with at least two new line characters.
# This is to prevent the last paragraph from a section being
# combined into the fist paragraph of the next.
end_chars = text[-4:]
# Convert all newlines to \n
end_chars = end_chars.replace('\r\n', '\n')
end_chars = end_chars.replace('\r', '\n')
end_chars = end_chars[-2:]
if not end_chars[1] == '\n':
text += '\n\n'
if end_chars[1] == '\n' and not end_chars[0] == '\n':
text += '\n'
output += text
output = u''.join(output)
return output

View File

@ -8,6 +8,7 @@ import os
from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation
from calibre.ebooks.txt.markdownml import MarkdownMLizer
from calibre.ebooks.txt.txtml import TXTMLizer
from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines
@ -44,10 +45,27 @@ class TXTOutput(OutputFormatPlugin):
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Force splitting on the max-line-length value when no space '
'is present. Also allows max-line-length to be below the minimum')),
OptionRecommendation(name='markdown_format',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Produce Markdown formatted text.')),
OptionRecommendation(name='keep_links',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Do not remove links within the document. This is only ' \
'useful when paired with the markdown-format option because' \
'links are always removed with plain text output.')),
OptionRecommendation(name='keep_image_references',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Do not remove image references within the document. This is only ' \
'useful when paired with the markdown-format option because' \
'image references are always removed with plain text output.')),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
writer = TXTMLizer(log)
if opts.markdown_format:
writer = MarkdownMLizer(log)
else:
writer = TXTMLizer(log)
txt = writer.extract_content(oeb_book, opts)
log.debug('\tReplacing newlines with selected type...')

View File

@ -35,6 +35,7 @@ BLOCK_STYLES = [
SPACE_TAGS = [
'td',
'br',
]
class TXTMLizer(object):

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os
from functools import partial
from PyQt4.Qt import Qt, QMenu
from PyQt4.Qt import Qt, QMenu, QModelIndex
from calibre.gui2 import error_dialog, config
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
@ -126,20 +126,35 @@ class EditMetadataAction(InterfaceAction):
if bulk or (bulk is None and len(rows) > 1):
return self.edit_bulk_metadata(checked)
def accepted(id):
self.gui.library_view.model().refresh_ids([id])
row_list = [r.row() for r in rows]
current_row = 0
changed = set([])
db = self.gui.library_view.model().db
for row in rows:
self.gui.iactions['View'].metadata_view_id = self.gui.library_view.model().db.id(row.row())
d = MetadataSingleDialog(self.gui, row.row(),
self.gui.library_view.model().db,
accepted_callback=accepted,
cancel_all=rows.index(row) < len(rows)-1)
d.view_format.connect(self.gui.iactions['View'].metadata_view_format)
d.exec_()
if d.cancel_all:
if len(row_list) == 1:
cr = row_list[0]
row_list = \
list(range(self.gui.library_view.model().rowCount(QModelIndex())))
current_row = row_list.index(cr)
while True:
prev = next_ = None
if current_row > 0:
prev = db.title(row_list[current_row-1])
if current_row < len(row_list) - 1:
next_ = db.title(row_list[current_row+1])
d = MetadataSingleDialog(self.gui, row_list[current_row], db,
prev=prev, next_=next_)
if d.exec_() != d.Accepted:
break
if rows:
changed.add(d.id)
if d.row_delta == 0:
break
current_row += d.row_delta
if changed:
self.gui.library_view.model().refresh_ids(list(changed))
current = self.gui.library_view.currentIndex()
m = self.gui.library_view.model()
if self.gui.cover_flow:

View File

@ -29,5 +29,6 @@ class ShowBookDetailsAction(InterfaceAction):
return
index = self.gui.library_view.currentIndex()
if index.isValid():
BookInfo(self.gui, self.gui.library_view, index).show()
BookInfo(self.gui, self.gui.library_view, index,
self.gui.iactions['View'].view_format_by_id).show()

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from calibre.gui2.convert.pmlz_output_ui import Ui_Form
from calibre.gui2.convert import Widget
format_model = None
class PluginWidget(Widget, Ui_Form):
TITLE = _('PMLZ Output')
HELP = _('Options specific to')+' PMLZ '+_('output')
COMMIT_NAME = 'pmlz_output'
ICON = I('mimetypes/unknown.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, ['inline_toc', 'full_image_depth'])
self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id)

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>246</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="opt_inline_toc">
<property name="text">
<string>&amp;Inline TOC</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="opt_full_image_depth">
<property name="text">
<string>Do not reduce image size and depth</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -21,7 +21,7 @@ class PluginWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent,
['newline', 'max_line_length', 'force_max_line_length',
'inline_toc'])
'inline_toc', 'markdown_format', 'keep_links', 'keep_image_references'])
self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id)

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<width>477</width>
<height>300</height>
</rect>
</property>
@ -27,7 +27,7 @@
<item row="0" column="1">
<widget class="QComboBox" name="opt_newline"/>
</item>
<item row="4" column="0">
<item row="7" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -67,6 +67,27 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="opt_markdown_format">
<property name="text">
<string>Apply Markdown formatting to text</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="opt_keep_links">
<property name="text">
<string>Do not remove links (&lt;a&gt; tags) before processing</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="opt_keep_image_references">
<property name="text">
<string>Do not remove image references before processing</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>

View File

@ -15,7 +15,7 @@ from PyQt4.Qt import QComboBox, QLabel, QSpinBox, QDoubleSpinBox, QDateEdit, \
from calibre.utils.date import qt_to_dt, now
from calibre.gui2.widgets import TagsLineEdit, EnComboBox
from calibre.gui2 import UNDEFINED_QDATE
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.utils.config import tweaks
class Base(object):
@ -310,6 +310,49 @@ class Series(Base):
self.db.set_custom(book_id, val, extra=s_index,
num=self.col_id, notify=notify, commit=False)
class Enumeration(Base):
def setup_ui(self, parent):
self.parent = parent
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QComboBox(parent)]
w = self.widgets[1]
vals = self.col_metadata['display']['enum_values']
w.addItem('')
for v in vals:
w.addItem(v)
def initialize(self, book_id):
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
val = self.normalize_db_val(val)
self.initial_val = val
idx = self.widgets[1].findText(val)
if idx < 0:
error_dialog(self.parent, '',
_('The enumeration "{0}" contains an invalid value '
'that will be set to the default').format(
self.col_metadata['name']),
show=True, show_copy_button=False)
idx = 0
self.widgets[1].setCurrentIndex(idx)
def setter(self, val):
self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
def getter(self):
return unicode(self.widgets[1].currentText())
def normalize_db_val(self, val):
if val is None:
val = ''
return val
def normalize_ui_val(self, val):
if not val:
val = None
return val
widgets = {
'bool' : Bool,
'rating' : Rating,
@ -319,6 +362,7 @@ widgets = {
'text' : Text,
'comments': Comments,
'series': Series,
'enumeration': Enumeration
}
def field_sort(y, z, x=None):
@ -551,6 +595,61 @@ class BulkSeries(BulkBase):
self.db.set_custom_bulk(book_ids, val, extras=extras,
num=self.col_id, notify=notify)
class BulkEnumeration(BulkBase, Enumeration):
def get_initial_value(self, book_ids):
value = None
ret_value = None
dialog_shown = False
for book_id in book_ids:
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
if val and val not in self.col_metadata['display']['enum_values']:
if not dialog_shown:
error_dialog(self.parent, '',
_('The enumeration "{0}" contains invalid values '
'that will not appear in the list').format(
self.col_metadata['name']),
show=True, show_copy_button=False)
dialog_shown = True
ret_value = ' nochange '
elif value is not None and value != val:
ret_value = ' nochange '
value = val
if ret_value is None:
return value
return ret_value
def setup_ui(self, parent):
self.parent = parent
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
QComboBox(parent)]
w = self.widgets[1]
vals = self.col_metadata['display']['enum_values']
w.addItem('Do Not Change')
w.addItem('')
for v in vals:
w.addItem(v)
def getter(self):
if self.widgets[1].currentIndex() == 0:
return ' nochange '
return unicode(self.widgets[1].currentText())
def setter(self, val):
if val == ' nochange ':
self.widgets[1].setCurrentIndex(0)
else:
if val is None:
self.widgets[1].setCurrentIndex(1)
else:
self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
def commit(self, book_ids, notify=False):
val = self.gui_val
val = self.normalize_ui_val(val)
if val != self.initial_val and val != ' nochange ':
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
class RemoveTags(QWidget):
def __init__(self, parent, values):
@ -656,4 +755,5 @@ bulk_widgets = {
'datetime': BulkDateTime,
'text' : BulkText,
'series': BulkSeries,
'enumeration': BulkEnumeration,
}

View File

@ -15,12 +15,13 @@ from calibre.library.comments import comments_to_html
class BookInfo(QDialog, Ui_BookInfo):
def __init__(self, parent, view, row):
def __init__(self, parent, view, row, view_func):
QDialog.__init__(self, parent)
Ui_BookInfo.__init__(self)
self.setupUi(self)
self.cover_pixmap = None
self.comments.sizeHint = self.comments_size_hint
self.view_func = view_func
desktop = QCoreApplication.instance().desktop()
screen_height = desktop.availableGeometry().height() - 100
@ -58,10 +59,7 @@ class BookInfo(QDialog, Ui_BookInfo):
if os.sep in path:
open_local_file(path)
else:
path = self.view.model().db.format_abspath(self.current_row, path)
if path is not None:
open_local_file(path)
self.view_func(self.view.model().id(self.current_row), path)
def next(self):
row = self.view.currentIndex().row()

View File

@ -255,7 +255,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
fm = self.db.field_metadata
for f in fm:
if (f in ['author_sort'] or
(fm[f]['datatype'] in ['text', 'series']
(fm[f]['datatype'] in ['text', 'series', 'enumeration']
and fm[f].get('search_terms', None)
and f not in ['formats', 'ondevice', 'sort'])):
self.all_fields.append(f)

View File

@ -7,9 +7,11 @@ add/remove formats
'''
import os, re, time, traceback, textwrap
from functools import partial
from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QThread, QDate, \
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QMessageBox
QPixmap, QListWidgetItem, QDialog, pyqtSignal, QMessageBox, QIcon, \
QPushButton
from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
choose_files, choose_images, ResizableDialog, \
@ -31,7 +33,7 @@ from calibre.gui2.preferences.social import SocialMetadata
from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre import strftime
class CoverFetcher(QThread):
class CoverFetcher(QThread): # {{{
def __init__(self, username, password, isbn, timeout, title, author):
self.username = username.strip() if username else username
@ -74,9 +76,9 @@ class CoverFetcher(QThread):
self.traceback = traceback.format_exc()
print self.traceback
# }}}
class Format(QListWidgetItem):
class Format(QListWidgetItem): # {{{
def __init__(self, parent, ext, size, path=None, timestamp=None):
self.path = path
@ -92,15 +94,70 @@ class Format(QListWidgetItem):
self.setToolTip(text)
self.setStatusTip(text)
# }}}
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
COVER_FETCH_TIMEOUT = 240 # seconds
view_format = pyqtSignal(object)
# Cover processing {{{
def set_cover(self):
mi, ext = self.get_selected_format_metadata()
if mi is None:
return
cdata = None
if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover).read()
elif mi.cover_data[1] is not None:
cdata = mi.cover_data[1]
if cdata is None:
error_dialog(self, _('Could not read cover'),
_('Could not read cover from %s format')%ext).exec_()
return
pix = QPixmap()
pix.loadFromData(cdata)
if pix.isNull():
error_dialog(self, _('Could not read cover'),
_('The cover in the %s format is invalid')%ext).exec_()
return
self.cover.setPixmap(pix)
self.update_cover_tooltip()
self.cover_changed = True
self.cpixmap = pix
self.cover_data = cdata
def trim_cover(self, *args):
from calibre.utils.magick import Image
cdata = self.cover_data
if not cdata:
return
im = Image()
im.load(cdata)
im.trim(10)
cdata = im.export('png')
pix = QPixmap()
pix.loadFromData(cdata)
self.cover.setPixmap(pix)
self.update_cover_tooltip()
self.cover_changed = True
self.cpixmap = pix
self.cover_data = cdata
def update_cover_tooltip(self):
p = self.cover.pixmap()
self.cover.setToolTip(_('Cover size: %dx%d pixels') %
(p.width(), p.height()))
def do_reset_cover(self, *args):
pix = QPixmap(I('default_cover.png'))
self.cover.setPixmap(pix)
self.update_cover_tooltip()
self.cover_changed = True
self.cover_data = None
@ -136,6 +193,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
else:
self.cover_path.setText(_file)
self.cover.setPixmap(pix)
self.update_cover_tooltip()
self.cover_changed = True
self.cpixmap = pix
self.cover_data = cover
@ -161,9 +219,80 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
pix = QPixmap()
pix.loadFromData(self.cover_data)
self.cover.setPixmap(pix)
self.update_cover_tooltip()
self.cover_changed = True
self.cpixmap = pix
def cover_dropped(self, cover_data):
self.cover_changed = True
self.cover_data = cover_data
self.update_cover_tooltip()
def fetch_cover(self):
isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text())).strip()
self.fetch_cover_button.setEnabled(False)
self.setCursor(Qt.WaitCursor)
title, author = map(unicode, (self.title.text(), self.authors.text()))
self.cover_fetcher = CoverFetcher(None, None, isbn,
self.timeout, title, author)
self.cover_fetcher.start()
self._hangcheck = QTimer(self)
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
self.cf_start_time = time.time()
self.pi.start(_('Downloading cover...'))
self._hangcheck.start(100)
def hangcheck(self):
if not self.cover_fetcher.isFinished() and \
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
return
self._hangcheck.stop()
try:
if self.cover_fetcher.isRunning():
self.cover_fetcher.terminate()
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+
_('The download timed out.')).exec_()
return
if self.cover_fetcher.needs_isbn:
error_dialog(self, _('Cannot fetch cover'),
_('Could not find cover for this book. Try '
'specifying the ISBN first.')).exec_()
return
if self.cover_fetcher.exception is not None:
err = self.cover_fetcher.exception
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
return
if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None:
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors])
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>') +
_('For the error message from each cover source, '
'click Show details below.'), det_msg=details, show=True)
return
pix = QPixmap()
pix.loadFromData(self.cover_fetcher.cover_data)
if pix.isNull():
error_dialog(self, _('Bad cover'),
_('The cover is not a valid picture')).exec_()
else:
self.cover.setPixmap(pix)
self.update_cover_tooltip()
self.cover_changed = True
self.cpixmap = pix
self.cover_data = self.cover_fetcher.cover_data
finally:
self.fetch_cover_button.setEnabled(True)
self.unsetCursor()
self.pi.stop()
# }}}
# Formats processing {{{
def add_format(self, x):
files = choose_files(self, 'add formats dialog',
_("Choose formats for ") + unicode((self.title.text())),
@ -276,48 +405,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.comments.setPlainText(mi.comments)
def set_cover(self):
mi, ext = self.get_selected_format_metadata()
if mi is None:
return
cdata = None
if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover).read()
elif mi.cover_data[1] is not None:
cdata = mi.cover_data[1]
if cdata is None:
error_dialog(self, _('Could not read cover'),
_('Could not read cover from %s format')%ext).exec_()
return
pix = QPixmap()
pix.loadFromData(cdata)
if pix.isNull():
error_dialog(self, _('Could not read cover'),
_('The cover in the %s format is invalid')%ext).exec_()
return
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
self.cover_data = cdata
def trim_cover(self, *args):
from calibre.utils.magick import Image
cdata = self.cover_data
if not cdata:
return
im = Image()
im.load(cdata)
im.trim(10)
cdata = im.export('png')
pix = QPixmap()
pix.loadFromData(cdata)
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
self.cover_data = cdata
def sync_formats(self):
old_extensions, new_extensions, paths = set(), set(), {}
for row in range(self.formats.count()):
@ -338,11 +425,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if ext not in extensions:
self.db.remove_format(self.row, ext, notify=False)
def do_cancel_all(self):
self.cancel_all = True
self.reject()
def show_format(self, item, *args):
fmt = item.ext
self.view_format.emit(fmt)
def __init__(self, window, row, db, accepted_callback=None, cancel_all=False):
# }}}
def __init__(self, window, row, db, prev=None,
next_=None):
ResizableDialog.__init__(self, window)
self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
self.cancel_all = False
@ -354,16 +444,27 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
_(' The red color indicates that the current '
'author sort does not match the current author'))
if cancel_all:
self.__abort_button = self.button_box.addButton(self.button_box.Abort)
self.__abort_button.setToolTip(_('Abort the editing of all remaining books'))
self.connect(self.__abort_button, SIGNAL('clicked()'),
self.do_cancel_all)
self.row_delta = 0
if prev:
self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
self)
self.button_box.addButton(self.prev_button, self.button_box.ActionRole)
tip = _('Save changes and edit the metadata of %s')%prev
self.prev_button.setToolTip(tip)
self.prev_button.clicked.connect(partial(self.next_triggered,
-1))
if next_:
self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
self)
self.button_box.addButton(self.next_button, self.button_box.ActionRole)
tip = _('Save changes and edit the metadata of %s')%next_
self.next_button.setToolTip(tip)
self.next_button.clicked.connect(partial(self.next_triggered, 1))
self.splitter.setStretchFactor(100, 1)
self.read_state()
self.db = db
self.pi = ProgressIndicator(self)
self.accepted_callback = accepted_callback
self.id = db.id(row)
self.row = row
self.cover_data = None
@ -412,6 +513,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.connect(self.reset_cover, SIGNAL('clicked()'), self.do_reset_cover)
self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author)
self.timeout = float(prefs['network_timeout'])
self.title.setText(db.title(row))
isbn = db.isbn(self.id, index_is_id=True)
if not isbn:
@ -472,6 +575,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
else:
self.cover_data = cover
self.cover.setPixmap(pm)
self.update_cover_tooltip()
self.original_series_name = unicode(self.series.text()).strip()
if len(db.custom_column_label_map) == 0:
self.central_widget.tabBar().setVisible(False)
@ -479,6 +583,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.create_custom_column_editors()
self.generate_cover_button.clicked.connect(self.generate_cover)
self.original_author = unicode(self.authors.text()).strip()
self.original_title = unicode(self.title.text()).strip()
def create_custom_column_editors(self):
w = self.central_widget.widget(1)
layout = w.layout()
@ -531,10 +638,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.isbn.setStyleSheet('QLineEdit { background-color: rgba(255,0,0,20%) }')
self.isbn.setToolTip(_('This ISBN number is invalid'))
def show_format(self, item, *args):
fmt = item.ext
self.view_format.emit(fmt)
def deduce_author_sort(self):
au = unicode(self.authors.text())
au = re.sub(r'\s+et al\.$', '', au)
@ -547,9 +650,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.authors.setText(title)
self.author_sort.setText('')
def cover_dropped(self, cover_data):
self.cover_changed = True
self.cover_data = cover_data
def initialize_combos(self):
self.initalize_authors()
@ -625,66 +725,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.tags.setText(tag_string)
self.tags.update_tags_cache(self.db.all_tags())
def fetch_cover(self):
isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text())).strip()
self.fetch_cover_button.setEnabled(False)
self.setCursor(Qt.WaitCursor)
title, author = map(unicode, (self.title.text(), self.authors.text()))
self.cover_fetcher = CoverFetcher(None, None, isbn,
self.timeout, title, author)
self.cover_fetcher.start()
self._hangcheck = QTimer(self)
self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck)
self.cf_start_time = time.time()
self.pi.start(_('Downloading cover...'))
self._hangcheck.start(100)
def hangcheck(self):
if not self.cover_fetcher.isFinished() and \
time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
return
self._hangcheck.stop()
try:
if self.cover_fetcher.isRunning():
self.cover_fetcher.terminate()
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+
_('The download timed out.')).exec_()
return
if self.cover_fetcher.needs_isbn:
error_dialog(self, _('Cannot fetch cover'),
_('Could not find cover for this book. Try '
'specifying the ISBN first.')).exec_()
return
if self.cover_fetcher.exception is not None:
err = self.cover_fetcher.exception
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
return
if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None:
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors])
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>') +
_('For the error message from each cover source, '
'click Show details below.'), det_msg=details, show=True)
return
pix = QPixmap()
pix.loadFromData(self.cover_fetcher.cover_data)
if pix.isNull():
error_dialog(self, _('Bad cover'),
_('The cover is not a valid picture')).exec_()
else:
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
self.cover_data = self.cover_fetcher.cover_data
finally:
self.fetch_cover_button.setEnabled(True)
self.unsetCursor()
self.pi.stop()
def fetch_metadata(self):
isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text()))
@ -778,6 +818,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
unicode(self.tags.text()).split(',')],
notify=notify, commit=commit)
def next_triggered(self, row_delta, *args):
self.row_delta = row_delta
self.accept()
def accept(self):
cf = getattr(self, 'cover_fetcher', None)
if cf is not None and hasattr(cf, 'terminate'):
@ -787,9 +831,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if self.formats_changed:
self.sync_formats()
title = unicode(self.title.text()).strip()
self.db.set_title(self.id, title, notify=False)
if title != self.original_title:
self.db.set_title(self.id, title, notify=False)
au = unicode(self.authors.text()).strip()
if au:
if au and au != self.original_author:
self.db.set_authors(self.id, string_to_authors(au), notify=False)
aus = unicode(self.author_sort.text()).strip()
if aus:
@ -839,8 +884,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
raise
self.save_state()
QDialog.accept(self)
if callable(self.accepted_callback):
self.accepted_callback(self.id)
def reject(self, *args):
cf = getattr(self, 'cover_fetcher', None)

View File

@ -254,6 +254,38 @@ class CcTextDelegate(QStyledItemDelegate): # {{{
# }}}
class CcEnumDelegate(QStyledItemDelegate): # {{{
'''
Delegate for text/int/float data.
'''
def createEditor(self, parent, option, index):
m = index.model()
col = m.column_map[index.column()]
editor = QComboBox(parent)
editor.addItem('')
for v in m.custom_columns[col]['display']['enum_values']:
editor.addItem(v)
return editor
def setModelData(self, editor, model, index):
val = unicode(editor.currentText())
if not val:
val = None
model.setData(index, QVariant(val), Qt.EditRole)
def setEditorData(self, editor, index):
m = index.model()
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
if val is None:
val = ''
idx = editor.findText(val)
if idx < 0:
editor.setCurrentIndex(0)
else:
editor.setCurrentIndex(idx)
# }}}
class CcCommentsDelegate(QStyledItemDelegate): # {{{
'''
Delegate for comments data.

View File

@ -634,7 +634,7 @@ class BooksModel(QAbstractTableModel): # {{{
for col in self.custom_columns:
idx = self.custom_columns[col]['rec_index']
datatype = self.custom_columns[col]['datatype']
if datatype in ('text', 'comments', 'composite'):
if datatype in ('text', 'comments', 'composite', 'enumeration'):
self.dc[col] = functools.partial(text_type, idx=idx,
mult=self.custom_columns[col]['is_multiple'])
elif datatype in ('int', 'float'):
@ -722,7 +722,11 @@ class BooksModel(QAbstractTableModel): # {{{
if typ in ('text', 'comments'):
val = unicode(value.toString()).strip()
val = val if val else None
if typ == 'bool':
elif typ == 'enumeration':
val = unicode(value.toString()).strip()
if not val:
val = None
elif typ == 'bool':
val = value.toPyObject()
elif typ == 'rating':
val = value.toInt()[0]
@ -730,7 +734,7 @@ class BooksModel(QAbstractTableModel): # {{{
val *= 2
elif typ in ('int', 'float'):
val = unicode(value.toString()).strip()
if val is None or not val:
if not val:
val = None
elif typ == 'datetime':
val = value.toDate()

View File

@ -14,7 +14,8 @@ from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate
CcBoolDelegate, CcCommentsDelegate, CcDateDelegate, CcTemplateDelegate, \
CcEnumDelegate
from calibre.gui2.library.models import BooksModel, DeviceBooksModel
from calibre.utils.config import tweaks, prefs
from calibre.gui2 import error_dialog, gprefs
@ -76,6 +77,7 @@ class BooksView(QTableView): # {{{
self.publisher_delegate = TextDelegate(self)
self.text_delegate = TextDelegate(self)
self.cc_text_delegate = CcTextDelegate(self)
self.cc_enum_delegate = CcEnumDelegate(self)
self.cc_bool_delegate = CcBoolDelegate(self)
self.cc_comments_delegate = CcCommentsDelegate(self)
self.cc_template_delegate = CcTemplateDelegate(self)
@ -427,6 +429,8 @@ class BooksView(QTableView): # {{{
self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
elif cc['datatype'] == 'composite':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_template_delegate)
elif cc['datatype'] == 'enumeration':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_enum_delegate)
else:
dattr = colhead+'_delegate'
delegate = colhead if hasattr(self, dattr) else 'text'

View File

@ -27,18 +27,20 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
3:{'datatype':'series',
'text':_('Text column for keeping series-like information'),
'is_multiple':False},
4:{'datatype':'datetime',
4:{'datatype':'enumeration',
'text':_('Text, but with a fixed set of permitted values'), 'is_multiple':False},
5:{'datatype':'datetime',
'text':_('Date'), 'is_multiple':False},
5:{'datatype':'float',
6:{'datatype':'float',
'text':_('Floating point numbers'), 'is_multiple':False},
6:{'datatype':'int',
7:{'datatype':'int',
'text':_('Integers'), 'is_multiple':False},
7:{'datatype':'rating',
8:{'datatype':'rating',
'text':_('Ratings, shown with stars'),
'is_multiple':False},
8:{'datatype':'bool',
9:{'datatype':'bool',
'text':_('Yes/No'), 'is_multiple':False},
9:{'datatype':'composite',
10:{'datatype':'composite',
'text':_('Column built from other columns'), 'is_multiple':False},
}
@ -59,6 +61,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
self.editing_col = editing
self.standard_colheads = standard_colheads
self.standard_colnames = standard_colnames
self.column_type_box.setMaxVisibleItems(len(self.column_types))
for t in self.column_types:
self.column_type_box.addItem(self.column_types[t]['text'])
self.column_type_box.currentIndexChanged.connect(self.datatype_changed)
@ -91,6 +94,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
self.date_format_box.setText(c['display'].get('date_format', ''))
elif ct == 'composite':
self.composite_box.setText(c['display'].get('composite_template', ''))
elif ct == 'enumeration':
self.enum_box.setText(','.join(c['display'].get('enum_values', [])))
self.datatype_changed()
self.exec_()
@ -103,7 +108,8 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
getattr(self, 'date_format_'+x).setVisible(col_type == 'datetime')
for x in ('box', 'default_label', 'label'):
getattr(self, 'composite_'+x).setVisible(col_type == 'composite')
for x in ('box', 'default_label', 'label'):
getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration')
def accept(self):
col = unicode(self.column_name_box.text())
@ -145,17 +151,31 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
return self.simple_error('', _('The heading %s is already used')%col_heading)
display_dict = {}
if col_type == 'datetime':
if self.date_format_box.text():
display_dict = {'date_format':unicode(self.date_format_box.text())}
else:
display_dict = {'date_format': None}
if col_type == 'composite':
elif col_type == 'composite':
if not self.composite_box.text():
return self.simple_error('', _('You must enter a template for'
' composite columns'))
display_dict = {'composite_template':unicode(self.composite_box.text())}
elif col_type == 'enumeration':
if not self.enum_box.text():
return self.simple_error('', _('You must enter at least one'
' value for enumeration columns'))
l = [v.strip() for v in unicode(self.enum_box.text()).split(',')]
for v in l:
if not v:
return self.simple_error('', _('You cannot provide the empty '
'value, as it is included by default'))
for i in range(0, len(l)-1):
if l[i] in l[i+1:]:
return self.simple_error('', _('The value "{0}" is in the '
'list more than once').format(l[i]))
display_dict = {'enum_values': l}
db = self.parent.gui.library_view.model().db
key = db.field_metadata.custom_field_prefix+col

View File

@ -10,7 +10,7 @@
<x>0</x>
<y>0</y>
<width>528</width>
<height>199</height>
<height>212</height>
</rect>
</property>
<property name="sizePolicy">
@ -24,7 +24,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0">
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0,0,0,0,0,0,0,0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
@ -56,7 +56,7 @@
</property>
</widget>
</item>
<item row="0" column="1">
<item row="0" column="2">
<widget class="QLineEdit" name="column_name_box">
<property name="minimumSize">
<size>
@ -69,7 +69,7 @@
</property>
</widget>
</item>
<item row="1" column="1">
<item row="1" column="2">
<widget class="QLineEdit" name="column_heading_box">
<property name="toolTip">
<string>Column heading in the library view and category name in the tag browser</string>
@ -86,7 +86,7 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="2" column="2">
<widget class="QComboBox" name="column_type_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
@ -105,7 +105,7 @@
</property>
</widget>
</item>
<item row="4" column="1">
<item row="4" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="date_format_box">
@ -147,7 +147,7 @@
</property>
</widget>
</item>
<item row="5" column="1">
<item row="5" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLineEdit" name="composite_box">
@ -158,7 +158,7 @@
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;p&gt;Field template. Uses the same syntax as save templates.</string>
<string>Field template. Uses the same syntax as save templates.</string>
</property>
</widget>
</item>
@ -184,7 +184,7 @@
</property>
</widget>
</item>
<item row="10" column="0" colspan="3">
<item row="11" column="0" colspan="4">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -197,6 +197,45 @@
</property>
</spacer>
</item>
<item row="6" column="0">
<widget class="QLabel" name="enum_label">
<property name="text">
<string>Values</string>
</property>
<property name="buddy">
<cstring>enum_box</cstring>
</property>
</widget>
</item>
<item row="6" column="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="enum_box">
<property name="toolTip">
<string>A comma-separated list of permitted values. The empty value is always
included, and is the default. For example, the list 'one,two,three' has
four values, the first of them being the empty value.</string>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="enum_default_label">
<property name="text">
<string>Default: (nothing)</string>
</property>
<property name="toolTip">
<string>The empty string is always the first value</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="11" column="0">

View File

@ -384,7 +384,7 @@ class SearchBoxMixin(object): # {{{
def do_advanced_search(self, *args):
d = SearchDialog(self, self.library_view.model().db)
if d.exec_() == QDialog.Accepted:
self.search.set_search_string(d.search_string())
self.search.set_search_string(d.search_string(), store_in_history=True)
def do_search_button(self):
self.search.do_search()

View File

@ -106,10 +106,13 @@ class TagsView(QTreeView): # {{{
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
self.sort_by.currentIndexChanged.connect(self.sort_changed)
self.made_connections = True
self.refresh_signal_processed = True
db.add_listener(self.database_changed)
def database_changed(self, event, ids):
self.refresh_required.emit()
if self.refresh_signal_processed:
self.refresh_signal_processed = False
self.refresh_required.emit()
@property
def match_all(self):
@ -295,6 +298,7 @@ class TagsView(QTreeView): # {{{
return self.isExpanded(idx)
def recount(self, *args):
self.refresh_signal_processed = True
ci = self.currentIndex()
if not ci.isValid():
ci = self.indexAt(QPoint(10, 10))
@ -937,7 +941,9 @@ class TagBrowserMixin(object): # {{{
if old_author != new_author:
# The id might change if the new author already exists
id = db.rename_author(id, new_author)
db.set_sort_field_for_author(id, unicode(new_sort))
db.set_sort_field_for_author(id, unicode(new_sort),
commit=False, notify=False)
db.commit()
self.library_view.model().refresh()
self.tags_view.recount()

View File

@ -614,7 +614,7 @@ class DocumentView(QWebView):
def search(self, text, backwards=False):
if backwards:
return self.findText(text, self.document.FindBackwards)
return self.findText(text, self.document.FindBackward)
return self.findText(text)
def path(self):

View File

@ -172,6 +172,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.iterator = None
self.current_page = None
self.pending_search = None
self.pending_search_dir= None
self.pending_anchor = None
self.pending_reference = None
self.pending_bookmark = None
@ -435,7 +436,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
if not text:
self.view.search('')
return self.search.search_done(False)
if self.view.search(text):
if self.view.search(text, backwards=backwards):
self.scrolled(self.view.scroll_fraction)
return self.search.search_done(True)
index = self.iterator.search(text, self.current_index,
@ -449,11 +450,13 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
return self.search.search_done(True)
return self.search.search_done(True)
self.pending_search = text
self.pending_search_dir = 'backwards' if backwards else 'forwards'
self.load_path(self.iterator.spine[index])
def do_search(self, text):
def do_search(self, text, backwards):
self.pending_search = None
if self.view.search(text):
self.pending_search_dir = None
if self.view.search(text, backwards=backwards):
self.scrolled(self.view.scroll_fraction)
def keyPressEvent(self, event):
@ -499,8 +502,10 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.current_index = index
self.set_page_number(self.view.scroll_fraction)
if self.pending_search is not None:
self.do_search(self.pending_search)
self.do_search(self.pending_search,
self.pending_search_dir=='backwards')
self.pending_search = None
self.pending_search_dir = None
if self.pending_anchor is not None:
self.view.scroll_to(self.pending_anchor)
self.pending_anchor = None

View File

@ -520,7 +520,7 @@ class ResultCache(SearchQueryParser): # {{{
if len(self.field_metadata[x]['search_terms']):
db_col[x] = self.field_metadata[x]['rec_index']
if self.field_metadata[x]['datatype'] not in \
['composite', 'text', 'comments', 'series']:
['composite', 'text', 'comments', 'series', 'enumeration']:
exclude_fields.append(db_col[x])
col_datatype[db_col[x]] = self.field_metadata[x]['datatype']
is_multiple_cols[db_col[x]] = self.field_metadata[x]['is_multiple']
@ -828,7 +828,7 @@ class SortKeyGenerator(object):
sidx = record[sidx_fm['rec_index']]
val = (val, sidx)
elif dt in ('text', 'comments', 'composite'):
elif dt in ('text', 'comments', 'composite', 'enumeration'):
if val is None:
val = ''
val = val.lower()

View File

@ -565,8 +565,9 @@ datatype is one of: {0}
'applies if datatype is text.'))
parser.add_option('--display', default='{}',
help=_('A dictionary of options to customize how '
'the data in this column will be interpreted.'))
'the data in this column will be interpreted. This is a JSON '
' string. For enumeration columns, use '
'--display=\'{"enum_values":["val1", "val2"]}\''))
return parser

View File

@ -18,7 +18,7 @@ from calibre.utils.date import parse_date
class CustomColumns(object):
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
'int', 'float', 'bool', 'series', 'composite'])
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
def custom_table_names(self, num):
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
@ -136,6 +136,12 @@ class CustomColumns(object):
x = bool(int(x))
return x
def adapt_enum(x, d):
v = adapt_text(x, d)
if not v:
v = None
return v
self.custom_data_adapters = {
'float': lambda x,d : x if x is None else float(x),
'int': lambda x,d : x if x is None else int(x),
@ -144,7 +150,8 @@ class CustomColumns(object):
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
'datetime' : adapt_datetime,
'text':adapt_text,
'series':adapt_text
'series':adapt_text,
'enumeration': adapt_enum
}
# Create Tag Browser categories for custom columns
@ -439,6 +446,9 @@ class CustomColumns(object):
val = self.custom_data_adapters[data['datatype']](val, data)
if data['normalized']:
if data['datatype'] == 'enumeration' and (
val and val not in data['display']['enum_values']):
return None
if not append or not data['is_multiple']:
self.conn.execute('DELETE FROM %s WHERE book=?'%lt, (id_,))
self.conn.execute(
@ -558,7 +568,7 @@ class CustomColumns(object):
if datatype in ('rating', 'int'):
dt = 'INT'
elif datatype in ('text', 'comments', 'series', 'composite'):
elif datatype in ('text', 'comments', 'series', 'composite', 'enumeration'):
dt = 'TEXT'
elif datatype in ('float',):
dt = 'REAL'

View File

@ -1639,15 +1639,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return []
return result
def set_sort_field_for_author(self, old_id, new_sort):
def set_sort_field_for_author(self, old_id, new_sort, commit=True, notify=False):
self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
(new_sort.strip(), old_id))
self.conn.commit()
if commit:
self.conn.commit()
# Now change all the author_sort fields in books by this author
bks = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,))
for (book_id,) in bks:
ss = self.author_sort_from_book(book_id, index_is_id=True)
self.set_author_sort(book_id, ss)
self.set_author_sort(book_id, ss, notify=notify, commit=commit)
def rename_author(self, old_id, new_name):
# Make sure that any commas in new_name are changed to '|'!

View File

@ -83,7 +83,7 @@ class FieldMetadata(dict):
'''
VALID_DATA_TYPES = frozenset([None, 'rating', 'text', 'comments', 'datetime',
'int', 'float', 'bool', 'series', 'composite'])
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
# Builtin metadata {{{
@ -177,7 +177,7 @@ class FieldMetadata(dict):
'is_multiple':None,
'kind':'field',
'name':None,
'search_terms':[],
'search_terms':['author_sort'],
'is_custom':False,
'is_category':False}),
('comments', {'table':None,

View File

@ -115,7 +115,6 @@ def pynocase(one, two, encoding='utf-8'):
pass
return cmp(one.lower(), two.lower())
def load_c_extensions(conn, debug=DEBUG):
try:
conn.enable_load_extension(True)

View File

@ -119,10 +119,11 @@ The functions available are:
* ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`.
* ``test(text if not empty, text if empty)`` -- return `text if not empty` if the field is not empty, otherwise return `text if empty`.
* ``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`.
* ``switch(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 ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
* ``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(&)}`
* ``lookup(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. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
* ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of |app|, these are python-compatible regular expressions.
* ``shorten(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 `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use ``{title:shorten(9,-,5)}``, the result will be `Ancient E-nhoe`. If the field's length is less than ``left chars`` + ``right chars`` + the length of ``middle text``, then the field will be used intact. For example, the title `The Dome` would not be changed.
* ``lookup(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. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
* ``switch(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 ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
Now, about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be::

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -79,6 +79,9 @@ class TemplateFormatter(string.Formatter):
else:
return val
def _count(self, val, sep):
return unicode(len(val.split(sep)))
functions = {
'uppercase' : (0, lambda s,x: x.upper()),
'lowercase' : (0, lambda s,x: x.lower()),
@ -91,6 +94,7 @@ class TemplateFormatter(string.Formatter):
'shorten' : (3, _shorten),
'switch' : (-1, _switch),
'test' : (2, _test),
'count' : (1, _count),
}
format_string_re = re.compile(r'^(.*)\|(.*)\|(.*)$')
@ -136,8 +140,13 @@ class TemplateFormatter(string.Formatter):
if fmt[colon:p] in self.functions:
field = fmt[colon:p]
func = self.functions[field]
args = self.arg_parser.scan(fmt[p+1:])[0]
args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
if func[0] == 1:
# only one arg expected. Don't bother to scan. Avoids need
# for escaping characters
args = [fmt[p+1:-1]]
else:
args = self.arg_parser.scan(fmt[p+1:])[0]
args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
if (func[0] == 0 and (len(args) != 1 or args[0])) or \
(func[0] > 0 and func[0] != len(args)):
raise ValueError('Incorrect number of arguments for function '+ fmt[0:p])