diff --git a/resources/images/news/DrawAndCook.png b/resources/images/news/DrawAndCook.png
new file mode 100644
index 0000000000..8b40b75344
Binary files /dev/null and b/resources/images/news/DrawAndCook.png differ
diff --git a/resources/recipes/DrawAndCook.recipe b/resources/recipes/DrawAndCook.recipe
index 1c080b85db..8db4f71014 100644
--- a/resources/recipes/DrawAndCook.recipe
+++ b/resources/recipes/DrawAndCook.recipe
@@ -1,8 +1,11 @@
from calibre.web.feeds.news import BasicNewsRecipe
+import re
class DrawAndCook(BasicNewsRecipe):
title = 'DrawAndCook'
__author__ = 'Starson17'
+ __version__ = 'v1.10'
+ __date__ = '13 March 2011'
description = 'Drawings of recipes!'
language = 'en'
publisher = 'Starson17'
@@ -13,6 +16,7 @@ class DrawAndCook(BasicNewsRecipe):
remove_javascript = True
remove_empty_feeds = True
cover_url = 'http://farm5.static.flickr.com/4043/4471139063_4dafced67f_o.jpg'
+ INDEX = 'http://www.theydrawandcook.com'
max_articles_per_feed = 30
remove_attributes = ['style', 'font']
@@ -34,20 +38,21 @@ class DrawAndCook(BasicNewsRecipe):
date = ''
current_articles = []
soup = self.index_to_soup(url)
- recipes = soup.findAll('div', attrs={'class': 'date-outer'})
+ featured_major_slider = soup.find(name='div', attrs={'id':'featured_major_slider'})
+ recipes = featured_major_slider.findAll('li', attrs={'data-id': re.compile(r'artwork_entry_\d+', re.DOTALL)})
for recipe in recipes:
- title = recipe.h3.a.string
- page_url = recipe.h3.a['href']
+ page_url = self.INDEX + recipe.a['href']
+ print 'page_url is: ', page_url
+ title = recipe.find('strong').string
+ print 'title is: ', title
current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':date})
return current_articles
-
- keep_only_tags = [dict(name='h3', attrs={'class':'post-title entry-title'})
- ,dict(name='div', attrs={'class':'post-body entry-content'})
+ keep_only_tags = [dict(name='h1', attrs={'id':'page_title'})
+ ,dict(name='section', attrs={'id':'artwork'})
]
- remove_tags = [dict(name='div', attrs={'class':['separator']})
- ,dict(name='div', attrs={'class':['post-share-buttons']})
+ remove_tags = [dict(name='article', attrs={'id':['recipe_actions', 'metadata']})
]
extra_css = '''
diff --git a/resources/recipes/instapaper.recipe b/resources/recipes/instapaper.recipe
index 73c32d08a7..0eb5cf0f09 100644
--- a/resources/recipes/instapaper.recipe
+++ b/resources/recipes/instapaper.recipe
@@ -1,23 +1,12 @@
-__license__ = 'GPL v3'
-__copyright__ = '2009-2010, Darko Miletic
Delete marked is used to remove extra files/folders/covers that have no entries in the database. Check the box next to the item you want to delete. Use with caution.
-Fix marked is applicable only to covers (the two lines marked - 'fixable'). In the case of missing cover files, checking the fixable - box and pushing this button will remove the cover mark from the - database for all the files in that category. In the case of extra - cover files, checking the fixable box and pushing this button will - add the cover mark to the database for all the files in that - category.
+ +Fix marked is applicable only to covers and missing formats + (the three lines marked 'fixable'). In the case of missing cover files, + checking the fixable box and pushing this button will tell calibre that + there is no cover for all of the books listed. Use this option if you + are not going to restore the covers from a backup. In the case of extra + cover files, checking the fixable box and pushing this button will tell + calibre that the cover files it found are correct for all the books + listed. Use this when you are not going to delete the file(s). In the + case of missing formats, checking the fixable box and pushing this + button will tell calibre that the formats are really gone. Use this if + you are not going to restore the formats from a backup.
+ ''')) self.log = QTreeWidget(self) @@ -381,6 +387,19 @@ class CheckLibraryDialog(QDialog): unicode(it.text(1)))) self.run_the_check() + def fix_missing_formats(self): + tl = self.top_level_items['missing_formats'] + child_count = tl.childCount() + for i in range(0, child_count): + item = tl.child(i); + id = item.data(0, Qt.UserRole).toInt()[0] + all = self.db.formats(id, index_is_id=True, verify_formats=False) + all = set([f.strip() for f in all.split(',')]) if all else set() + valid = self.db.formats(id, index_is_id=True, verify_formats=True) + valid = set([f.strip() for f in valid.split(',')]) if valid else set() + for fmt in all-valid: + self.db.remove_format(id, fmt, index_is_id=True, db_only=True) + def fix_missing_covers(self): tl = self.top_level_items['missing_covers'] child_count = tl.childCount() diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index d918991aad..9b25545252 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -783,6 +783,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): books_to_refresh = self.db.set_custom(id, val, label=dfm['label'], extra=extra, commit=False, allow_case_change=True) + elif dest.startswith('#') and dest.endswith('_index'): + label = self.db.field_metadata[dest[:-6]]['label'] + series = self.db.get_custom(id, label=label, index_is_id=True) + books_to_refresh = self.db.set_custom(id, series, label=label, + extra=val, commit=False, + allow_case_change=True) else: if dest == 'comments': setter = self.db.set_comment diff --git a/src/calibre/gui2/dialogs/saved_search_editor.py b/src/calibre/gui2/dialogs/saved_search_editor.py index 1143a6f06a..c9f843109a 100644 --- a/src/calibre/gui2/dialogs/saved_search_editor.py +++ b/src/calibre/gui2/dialogs/saved_search_editor.py @@ -9,12 +9,13 @@ from PyQt4.QtGui import QDialog from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor from calibre.utils.search_query_parser import saved_searches from calibre.utils.icu import sort_key +from calibre.gui2 import error_dialog from calibre.gui2.dialogs.confirm_delete import confirm class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): - def __init__(self, window, initial_search=None): - QDialog.__init__(self, window) + def __init__(self, parent, initial_search=None): + QDialog.__init__(self, parent) Ui_SavedSearchEditor.__init__(self) self.setupUi(self) @@ -22,12 +23,13 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'), self.current_index_changed) self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search) + self.rename_button.clicked.connect(self.rename_search) self.current_search_name = None self.searches = {} - self.searches_to_delete = [] for name in saved_searches().names(): self.searches[name] = saved_searches().lookup(name) + self.search_names = set([icu_lower(n) for n in saved_searches().names()]) self.populate_search_list() if initial_search is not None and initial_search in self.searches: @@ -42,6 +44,11 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): search_name = unicode(self.input_box.text()).strip() if search_name == '': return False + if icu_lower(search_name) in self.search_names: + error_dialog(self, _('Saved search already exists'), + _('The saved search %s already exists, perhaps with ' + 'different case')%search_name).exec_() + return False if search_name not in self.searches: self.searches[search_name] = '' self.populate_search_list() @@ -57,10 +64,25 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): +'', 'saved_search_editor_delete', self): return del self.searches[self.current_search_name] - self.searches_to_delete.append(self.current_search_name) self.current_search_name = None self.search_name_box.removeItem(self.search_name_box.currentIndex()) + def rename_search(self): + new_search_name = unicode(self.input_box.text()).strip() + if new_search_name == '': + return False + if icu_lower(new_search_name) in self.search_names: + error_dialog(self, _('Saved search already exists'), + _('The saved search %s already exists, perhaps with ' + 'different case')%new_search_name).exec_() + return False + if self.current_search_name in self.searches: + self.searches[new_search_name] = self.searches[self.current_search_name] + del self.searches[self.current_search_name] + self.populate_search_list() + self.select_search(new_search_name) + return True + def select_search(self, name): self.search_name_box.setCurrentIndex(self.search_name_box.findText(name)) @@ -78,7 +100,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): def accept(self): if self.current_search_name: self.searches[self.current_search_name] = unicode(self.search_text.toPlainText()) - for name in self.searches_to_delete: + for name in saved_searches().names(): saved_searches().delete(name) for name in self.searches: saved_searches().add(name, self.searches[name]) diff --git a/src/calibre/gui2/dialogs/saved_search_editor.ui b/src/calibre/gui2/dialogs/saved_search_editor.ui index 3ba37bdf10..99672b5b8e 100644 --- a/src/calibre/gui2/dialogs/saved_search_editor.ui +++ b/src/calibre/gui2/dialogs/saved_search_editor.ui @@ -134,6 +134,20 @@ +' +
_('You have started calibre in debug mode. After you '
@@ -399,6 +402,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
elif msg.startswith('refreshdb:'):
self.library_view.model().refresh()
self.library_view.model().research()
+ self.tags_view.recount()
else:
print msg
@@ -463,6 +467,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.card_a_view.reset()
self.card_b_view.reset()
self.device_manager.set_current_library_uuid(db.library_id)
+ # Run a garbage collection now so that it does not freeze the
+ # interface later
+ gc.collect()
def set_window_title(self):
diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py
index 964616ab48..c704b98dc9 100644
--- a/src/calibre/gui2/viewer/main.py
+++ b/src/calibre/gui2/viewer/main.py
@@ -225,6 +225,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.action_quit.setShortcuts(qs)
self.connect(self.action_quit, SIGNAL('triggered(bool)'),
lambda x:QApplication.instance().quit())
+ self.action_focus_search = QAction(self)
+ self.addAction(self.action_focus_search)
+ self.action_focus_search.setShortcuts([Qt.Key_Slash,
+ QKeySequence(QKeySequence.Find)])
+ self.action_focus_search.triggered.connect(lambda x:
+ self.search.setFocus(Qt.OtherFocusReason))
self.action_copy.setDisabled(True)
self.action_metadata.setCheckable(True)
self.action_metadata.setShortcut(Qt.CTRL+Qt.Key_I)
@@ -293,6 +299,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
ca.setShortcut(QKeySequence.Copy)
self.addAction(ca)
self.open_history_menu = QMenu()
+ self.clear_recent_history_action = QAction(
+ _('Clear list of recently opened books'), self)
+ self.clear_recent_history_action.triggered.connect(self.clear_recent_history)
self.build_recent_menu()
self.action_open_ebook.setMenu(self.open_history_menu)
self.open_history_menu.triggered[QAction].connect(self.open_recent)
@@ -301,11 +310,19 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.restore_state()
+ def clear_recent_history(self, *args):
+ vprefs.set('viewer_open_history', [])
+ self.build_recent_menu()
+
def build_recent_menu(self):
m = self.open_history_menu
m.clear()
+ recent = vprefs.get('viewer_open_history', [])
+ if recent:
+ m.addAction(self.clear_recent_history_action)
+ m.addSeparator()
count = 0
- for path in vprefs.get('viewer_open_history', []):
+ for path in recent:
if count > 9:
break
if os.path.exists(path):
@@ -494,12 +511,6 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
if self.view.search(text, backwards=backwards):
self.scrolled(self.view.scroll_fraction)
- def keyPressEvent(self, event):
- if event.key() == Qt.Key_Slash:
- self.search.setFocus(Qt.OtherFocusReason)
- else:
- return MainWindow.keyPressEvent(self, event)
-
def internal_link_clicked(self, frac):
self.history.add(self.pos.value())
diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py
index 5785f52276..44cd8dd2e4 100644
--- a/src/calibre/gui2/wizard/send_email.py
+++ b/src/calibre/gui2/wizard/send_email.py
@@ -92,7 +92,8 @@ class SendEmail(QWidget, Ui_Form):
pa = self.preferred_to_address()
to_set = pa is not None
if self.set_email_settings(to_set):
- if question_dialog(self, _('OK to proceed?'),
+ opts = smtp_prefs().parse()
+ if not opts.relay_password or question_dialog(self, _('OK to proceed?'),
_('This will display your email password on the screen'
'. Is it OK to proceed?'), show_copy_button=False):
TestEmail(pa, self).exec_()
@@ -204,19 +205,32 @@ class SendEmail(QWidget, Ui_Form):
username = unicode(self.relay_username.text()).strip()
password = unicode(self.relay_password.text()).strip()
host = unicode(self.relay_host.text()).strip()
- if host and not (username and password):
- error_dialog(self, _('Bad configuration'),
- _('You must set the username and password for '
- 'the mail server.')).exec_()
- return False
+ enc_method = ('TLS' if self.relay_tls.isChecked() else 'SSL'
+ if self.relay_ssl.isChecked() else 'NONE')
+ if host:
+ # Validate input
+ if ((username and not password) or (not username and password)):
+ error_dialog(self, _('Bad configuration'),
+ _('You must either set both the username and password for '
+ 'the mail server or no username and no password at all.')).exec_()
+ return False
+ if not username and not password and enc_method != 'NONE':
+ error_dialog(self, _('Bad configuration'),
+ _('Please enter a username and password or set'
+ ' encryption to None ')).exec_()
+ return False
+ if not (username and password) and not question_dialog(self,
+ _('Are you sure?'),
+ _('No username and password set for mailserver. Most '
+ ' mailservers need a username and password. Are you sure?')):
+ return False
conf = smtp_prefs()
conf.set('from_', from_)
conf.set('relay_host', host if host else None)
conf.set('relay_port', self.relay_port.value())
conf.set('relay_username', username if username else None)
conf.set('relay_password', hexlify(password))
- conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL'
- if self.relay_ssl.isChecked() else 'NONE')
+ conf.set('encryption', enc_method)
return True
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index 97ddaeb51a..be996063d5 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -123,14 +123,22 @@ REGEXP_MATCH = 2
def _match(query, value, matchkind):
if query.startswith('..'):
query = query[1:]
- prefix_match_ok = False
+ sq = query[1:]
+ internal_match_ok = True
else:
- prefix_match_ok = True
+ internal_match_ok = False
for t in value:
t = icu_lower(t)
try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished
if (matchkind == EQUALS_MATCH):
- if prefix_match_ok and query[0] == '.':
+ if internal_match_ok:
+ if query == t:
+ return True
+ comps = [c.strip() for c in t.split('.') if c.strip()]
+ for comp in comps:
+ if sq == comp:
+ return True
+ elif query[0] == '.':
if t.startswith(query[1:]):
ql = len(query) - 1
if (len(t) == ql) or (t[ql:ql+1] == '.'):
diff --git a/src/calibre/library/check_library.py b/src/calibre/library/check_library.py
index 19ecb97308..6013c76b9c 100644
--- a/src/calibre/library/check_library.py
+++ b/src/calibre/library/check_library.py
@@ -27,7 +27,7 @@ CHECKS = [('invalid_titles', _('Invalid titles'), True, False),
('extra_titles', _('Extra titles'), True, False),
('invalid_authors', _('Invalid authors'), True, False),
('extra_authors', _('Extra authors'), True, False),
- ('missing_formats', _('Missing book formats'), False, False),
+ ('missing_formats', _('Missing book formats'), False, True),
('extra_formats', _('Extra book formats'), True, False),
('extra_files', _('Unknown files in books'), True, False),
('missing_covers', _('Missing covers files'), False, True),
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index d03975baea..ec766c72f3 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -56,7 +56,7 @@ class Tag(object):
self.is_hierarchical = False
self.is_editable = is_editable
self.is_searchable = is_searchable
- self.id_set = id_set
+ self.id_set = id_set if id_set is not None else set([])
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
if self.avg_rating > 0:
@@ -1154,15 +1154,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if notify:
self.notify('delete', [id])
- def remove_format(self, index, format, index_is_id=False, notify=True, commit=True):
+ def remove_format(self, index, format, index_is_id=False, notify=True,
+ commit=True, db_only=False):
id = index if index_is_id else self.id(index)
name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False)
if name:
- path = self.format_abspath(id, format, index_is_id=True)
- try:
- delete_file(path)
- except:
- traceback.print_exc()
+ if not db_only:
+ try:
+ path = self.format_abspath(id, format, index_is_id=True)
+ if path:
+ delete_file(path)
+ except:
+ traceback.print_exc()
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, format.upper()))
if commit:
self.conn.commit()
@@ -1690,10 +1693,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id])
return books_to_refresh
- def set_metadata(self, id, mi, ignore_errors=False,
- set_title=True, set_authors=True, commit=True):
+ def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
+ set_authors=True, commit=True, force_changes=False):
'''
Set metadata for the book `id` from the `Metadata` object `mi`
+
+ Setting force_changes=True will force set_metadata to update fields even
+ if mi contains empty values. In this case, 'None' is distinguished from
+ 'empty'. If mi.XXX is None, the XXX is not replaced, otherwise it is.
+ The tags, identifiers, and cover attributes are special cases. Tags and
+ identifiers cannot be set to None so then will always be replaced if
+ force_changes is true. You must ensure that mi contains the values you
+ want the book to have. Covers are always changed if a new cover is
+ provided, but are never deleted. Also note that force_changes has no
+ effect on setting title or authors.
'''
if callable(getattr(mi, 'to_book_metadata', None)):
# Handle code passing in a OPF object instead of a Metadata object
@@ -1707,6 +1720,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc()
else:
raise
+
+ def should_replace_field(attr):
+ return (force_changes and (mi.get(attr, None) is not None)) or \
+ not mi.is_null(attr)
+
path_changed = False
if set_title and mi.title:
self._set_title(id, mi.title)
@@ -1721,16 +1739,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
path_changed = True
if path_changed:
self.set_path(id, index_is_id=True)
- if mi.author_sort:
+
+ if should_replace_field('author_sort'):
doit(self.set_author_sort, id, mi.author_sort, notify=False,
commit=False)
- if mi.publisher:
+ if should_replace_field('publisher'):
doit(self.set_publisher, id, mi.publisher, notify=False,
commit=False)
- if mi.rating:
+
+ # Setting rating to zero is acceptable.
+ if mi.rating is not None:
doit(self.set_rating, id, mi.rating, notify=False, commit=False)
- if mi.series:
+ if should_replace_field('series'):
doit(self.set_series, id, mi.series, notify=False, commit=False)
+
+ # force_changes has no effect on cover manipulation
if mi.cover_data[1] is not None:
doit(self.set_cover, id, mi.cover_data[1], commit=False)
elif mi.cover is not None:
@@ -1739,21 +1762,30 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
raw = f.read()
if raw:
doit(self.set_cover, id, raw, commit=False)
- if mi.tags:
+
+ # if force_changes is true, tags are always replaced because the
+ # attribute cannot be set to None.
+ if should_replace_field('tags'):
doit(self.set_tags, id, mi.tags, notify=False, commit=False)
- if mi.comments:
+
+ if should_replace_field('comments'):
doit(self.set_comment, id, mi.comments, notify=False, commit=False)
- if mi.series_index:
+
+ # Setting series_index to zero is acceptable
+ if mi.series_index is not None:
doit(self.set_series_index, id, mi.series_index, notify=False,
commit=False)
- if mi.pubdate:
+ if should_replace_field('pubdate'):
doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False)
if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False,
commit=False)
+ # identifiers will always be replaced if force_changes is True
mi_idents = mi.get_identifiers()
- if mi_idents:
+ if force_changes:
+ self.set_identifiers(id, mi_idents, notify=False, commit=False)
+ elif mi_idents:
identifiers = self.get_identifiers(id, index_is_id=True)
for key, val in mi_idents.iteritems():
if val and val.strip(): # Don't delete an existing identifier
@@ -1765,10 +1797,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for key in user_mi.iterkeys():
if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
- doit(self.set_custom, id,
- val=mi.get(key),
- extra=mi.get_extra(key),
- label=user_mi[key]['label'], commit=False)
+ val = mi.get(key, None)
+ if force_changes or val is not None:
+ doit(self.set_custom, id, val=val, extra=mi.get_extra(key),
+ label=user_mi[key]['label'], commit=False)
if commit:
self.conn.commit()
self.notify('metadata', [id])
@@ -2358,6 +2390,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
@param tags: list of strings
@param append: If True existing tags are not removed
'''
+ if not tags:
+ tags = []
if not append:
self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
self.conn.execute('''DELETE FROM tags WHERE (SELECT COUNT(id)
@@ -2508,6 +2542,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id])
def set_rating(self, id, rating, notify=True, commit=True):
+ if not rating:
+ rating = 0
rating = int(rating)
self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,))
rat = self.conn.get('SELECT id FROM ratings WHERE rating=?', (rating,), all=False)
@@ -2522,7 +2558,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def set_comment(self, id, text, notify=True, commit=True):
self.conn.execute('DELETE FROM comments WHERE book=?', (id,))
- self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text))
+ if text:
+ self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text))
+ else:
+ text = ''
if commit:
self.conn.commit()
self.data.set(id, self.FIELD_MAP['comments'], text, row_is_id=True)
@@ -2531,6 +2570,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id])
def set_author_sort(self, id, sort, notify=True, commit=True):
+ if not sort:
+ sort = ''
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (sort, id))
self.dirtied([id], commit=False)
if commit:
@@ -2602,6 +2643,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def set_identifiers(self, id_, identifiers, notify=True, commit=True):
cleaned = {}
+ if not identifiers:
+ identifiers = {}
for typ, val in identifiers.iteritems():
typ, val = self._clean_identifier(typ, val)
if val:
diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py
index 97bfc30f14..fd015f5848 100644
--- a/src/calibre/library/server/browse.py
+++ b/src/calibre/library/server/browse.py
@@ -12,7 +12,7 @@ import cherrypy
from calibre.constants import filesystem_encoding
from calibre import isbytestring, force_unicode, fit_image, \
- prepare_string_for_xml as xml
+ prepare_string_for_xml
from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.filenames import ascii_filename
from calibre.utils.config import prefs, tweaks
@@ -23,6 +23,10 @@ from calibre.library.server import custom_fields_to_display
from calibre.library.field_metadata import category_icon_map
from calibre.library.server.utils import quote, unquote
+def xml(*args, **kwargs):
+ ans = prepare_string_for_xml(*args, **kwargs)
+ return ans.replace(''', ''')
+
def render_book_list(ids, prefix, suffix=''): # {{{
pages = []
num = len(ids)
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index a3d4332fd0..948611f775 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -508,9 +508,9 @@ You have two choices:
1. Create a patch by hacking on |app| and send it to me for review and inclusion. See `Development