'
+from PyQt4.QtCore import SIGNAL, Qt
+from PyQt4.QtGui import QDialog, QListWidgetItem
+
+from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
+from calibre.gui2 import question_dialog, error_dialog
+
+class TagListEditor(QDialog, Ui_TagListEditor):
+
+ def tag_cmp(self, x, y):
+ return cmp(x.lower(), y.lower())
+
+ def __init__(self, window, db, tag_to_match):
+ QDialog.__init__(self, window)
+ Ui_TagListEditor.__init__(self)
+ self.setupUi(self)
+
+ self.to_rename = {}
+ self.to_delete = []
+ self.db = db
+ self.all_tags = {}
+ for k,v in db.get_tags_with_ids():
+ self.all_tags[v] = k
+ for tag in sorted(self.all_tags.keys(), cmp=self.tag_cmp):
+ item = QListWidgetItem(tag)
+ item.setData(Qt.UserRole, self.all_tags[tag])
+ self.available_tags.addItem(item)
+
+ items = self.available_tags.findItems(tag_to_match, Qt.MatchExactly)
+ if len(items) == 1:
+ self.available_tags.setCurrentItem(items[0])
+
+ self.connect(self.delete_button, SIGNAL('clicked()'), self.delete_tags)
+ self.connect(self.rename_button, SIGNAL('clicked()'), self.rename_tag)
+ self.connect(self.available_tags, SIGNAL('itemDoubleClicked(QListWidgetItem *)'), self._rename_tag)
+ self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing)
+
+ def finish_editing(self, item):
+ if item.text() != self.item_before_editing.text():
+ if item.text() in self.all_tags.keys() or item.text() in self.to_rename.keys():
+ error_dialog(self, 'Tag already used',
+ 'The tag %s is already used.'%(item.text())).exec_()
+ item.setText(self.item_before_editing.text())
+ return
+ id,ign = self.item_before_editing.data(Qt.UserRole).toInt()
+ self.to_rename[item.text()] = id
+
+ def rename_tag(self):
+ item = self.available_tags.currentItem()
+ self._rename_tag(item)
+
+ def _rename_tag(self, item):
+ if item is None:
+ error_dialog(self, 'No tag selected', 'You must select one tag from the list of Available tags.').exec_()
+ return
+ self.item_before_editing = item.clone()
+ item.setFlags (item.flags() | Qt.ItemIsEditable);
+ self.available_tags.editItem(item)
+
+ def delete_tags(self, item=None):
+ confirms, deletes = [], []
+ items = self.available_tags.selectedItems() if item is None else [item]
+ if not items:
+ error_dialog(self, 'No tags selected', 'You must select at least one tag from the list of Available tags.').exec_()
+ return
+ for item in items:
+ if self.db.is_tag_used(unicode(item.text())):
+ confirms.append(item)
+ else:
+ deletes.append(item)
+ if confirms:
+ ct = ', '.join([unicode(item.text()) for item in confirms])
+ if question_dialog(self, _('Are your sure?'),
+ ''+_('The following tags are used by one or more books. '
+ 'Are you certain you want to delete them?')+'
'+ct):
+ deletes += confirms
+
+ for item in deletes:
+ self.to_delete.append(item)
+ self.available_tags.takeItem(self.available_tags.row(item))
+
+ def accept(self):
+ for text in self.to_rename:
+ self.db.rename_tag(self.to_rename[text], unicode(text))
+ for item in self.to_delete:
+ self.db.delete_tag(unicode(item.text()))
+ QDialog.accept(self)
+
diff --git a/src/calibre/gui2/dialogs/tag_list_editor.ui b/src/calibre/gui2/dialogs/tag_list_editor.ui
new file mode 100644
index 0000000000..383dc875ac
--- /dev/null
+++ b/src/calibre/gui2/dialogs/tag_list_editor.ui
@@ -0,0 +1,163 @@
+
+
+ TagListEditor
+
+
+
+ 0
+ 0
+ 397
+ 335
+
+
+
+ Tag Editor
+
+
+
+ :/images/chapters.svg:/images/chapters.svg
+
+
+ -
+
+
-
+
+
-
+
+
+ Tags in use
+
+
+ available_tags
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
-
+
+
+ Delete tag from database. This will unapply the tag from all books and then remove it from the database.
+
+
+ ...
+
+
+
+ :/images/trash.svg:/images/trash.svg
+
+
+
+ 32
+ 32
+
+
+
+
+ -
+
+
+ Rename the tag everywhere it is used.
+
+
+ ...
+
+
+
+ :/images/edit_input.svg:/images/edit_input.svg
+
+
+
+ 32
+ 32
+
+
+
+ Ctrl+S
+
+
+
+
+
+ -
+
+
+ true
+
+
+ QAbstractItemView::ExtendedSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ TagListEditor
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ TagListEditor
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py
index 73f7f3839d..29ae1875c8 100644
--- a/src/calibre/gui2/main.py
+++ b/src/calibre/gui2/main.py
@@ -5,13 +5,15 @@ import sys, os, time, socket, traceback
from functools import partial
from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \
- QThread, pyqtSignal, Qt, QProgressDialog, QString
+ QThread, pyqtSignal, Qt, QProgressDialog, QString, QPixmap, \
+ QSplashScreen, QApplication
from calibre import prints, plugins
-from calibre.constants import iswindows, __appname__, isosx, filesystem_encoding
+from calibre.constants import iswindows, __appname__, isosx, DEBUG, \
+ filesystem_encoding
from calibre.utils.ipc import ADDRESS, RC
from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \
- Application, choose_dir, error_dialog, question_dialog
+ Application, choose_dir, error_dialog, question_dialog, gprefs
from calibre.gui2.main_window import option_parser as _option_parser
from calibre.utils.config import prefs, dynamic
from calibre.library.database2 import LibraryDatabase2
@@ -113,16 +115,25 @@ class GuiRunner(QObject):
initialization'''
def __init__(self, opts, args, actions, listener, app):
+ self.startup_time = time.time()
self.opts, self.args, self.listener, self.app = opts, args, listener, app
self.actions = actions
self.main = None
QObject.__init__(self)
+ self.splash_screen = None
self.timer = QTimer.singleShot(1, self.initialize)
+ if DEBUG:
+ prints('Starting up...')
def start_gui(self):
from calibre.gui2.ui import Main
main = Main(self.opts)
+ if self.splash_screen is not None:
+ self.splash_screen.showMessage(_('Initializing user interface...'))
+ self.splash_screen.finish(main)
main.initialize(self.library_path, self.db, self.listener, self.actions)
+ if DEBUG:
+ prints('Started up in', time.time() - self.startup_time)
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
sys.excepthook = main.unhandled_exception
if len(self.args) > 1:
@@ -143,7 +154,7 @@ class GuiRunner(QObject):
if db is None and tb is not None:
# DB Repair failed
- error_dialog(None, _('Repairing failed'),
+ error_dialog(self.splash_screen, _('Repairing failed'),
_('The database repair failed. Starting with '
'a new empty library.'),
det_msg=tb, show=True)
@@ -160,7 +171,7 @@ class GuiRunner(QObject):
os.makedirs(x)
except:
x = os.path.expanduser('~')
- candidate = choose_dir(None, 'choose calibre library',
+ candidate = choose_dir(self.splash_screen, 'choose calibre library',
_('Choose a location for your new calibre e-book library'),
default_dir=x)
@@ -171,7 +182,7 @@ class GuiRunner(QObject):
self.library_path = candidate
db = LibraryDatabase2(candidate)
except:
- error_dialog(None, _('Bad database location'),
+ error_dialog(self.splash_screen, _('Bad database location'),
_('Bad database location %r. calibre will now quit.'
)%self.library_path,
det_msg=traceback.format_exc(), show=True)
@@ -185,7 +196,7 @@ class GuiRunner(QObject):
try:
db = LibraryDatabase2(self.library_path)
except (sqlite.Error, DatabaseException):
- repair = question_dialog(None, _('Corrupted database'),
+ repair = question_dialog(self.splash_screen, _('Corrupted database'),
_('Your calibre database appears to be corrupted. Do '
'you want calibre to try and repair it automatically? '
'If you say No, a new empty calibre library will be created.'),
@@ -204,14 +215,27 @@ class GuiRunner(QObject):
self.repair.start()
return
except:
- error_dialog(None, _('Bad database location'),
+ error_dialog(self.splash_screen, _('Bad database location'),
_('Bad database location %r. Will start with '
' a new, empty calibre library')%self.library_path,
det_msg=traceback.format_exc(), show=True)
self.initialize_db_stage2(db, None)
+ def show_splash_screen(self):
+ self.splash_pixmap = QPixmap()
+ self.splash_pixmap.load(I('library.png'))
+ self.splash_screen = QSplashScreen(self.splash_pixmap,
+ Qt.SplashScreen|Qt.WindowStaysOnTopHint)
+ self.splash_screen.showMessage(_('Starting %s: Loading books...') %
+ __appname__)
+ self.splash_screen.show()
+ QApplication.instance().processEvents()
+
def initialize(self, *args):
+ if gprefs.get('show_splash_screen', True):
+ self.show_splash_screen()
+
self.library_path = get_library_path()
if self.library_path is None:
self.initialization_failed()
diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py
index 15270e14b1..11db157ed4 100644
--- a/src/calibre/gui2/tag_view.py
+++ b/src/calibre/gui2/tag_view.py
@@ -8,10 +8,11 @@ Browsing book collection by tags.
'''
from itertools import izip
+from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QFont, QSize, QIcon, QPoint, \
- QAbstractItemModel, QVariant, QModelIndex
+ QAbstractItemModel, QVariant, QModelIndex, QMenu
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.library.field_metadata import TagsIcons
@@ -19,9 +20,12 @@ from calibre.utils.search_query_parser import saved_searches
class TagsView(QTreeView): # {{{
- need_refresh = pyqtSignal()
- restriction_set = pyqtSignal(object)
- tags_marked = pyqtSignal(object, object)
+ need_refresh = pyqtSignal()
+ restriction_set = pyqtSignal(object)
+ tags_marked = pyqtSignal(object, object)
+ user_category_edit = pyqtSignal(object)
+ tag_list_edit = pyqtSignal(object)
+ saved_search_edit = pyqtSignal(object)
def __init__(self, *args):
QTreeView.__init__(self, *args)
@@ -31,13 +35,16 @@ class TagsView(QTreeView): # {{{
self.tag_match = None
def set_database(self, db, tag_match, popularity, restriction):
- self._model = TagsModel(db, parent=self)
+ self.hidden_categories = config['tag_browser_hidden_categories']
+ self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories)
self.popularity = popularity
self.restriction = restriction
self.tag_match = tag_match
self.db = db
self.setModel(self._model)
+ self.setContextMenuPolicy(Qt.CustomContextMenu)
self.clicked.connect(self.toggle)
+ self.customContextMenuRequested.connect(self.show_context_menu)
self.popularity.setChecked(config['sort_by_popularity'])
self.popularity.stateChanged.connect(self.sort_changed)
self.restriction.activated[str].connect(self.search_restriction_set)
@@ -45,10 +52,6 @@ class TagsView(QTreeView): # {{{
db.add_listener(self.database_changed)
self.saved_searches_changed(recount=False)
- def create_tag_category(self, name, tag_list):
- self._model.create_tag_category(name, tag_list)
- self.recount()
-
def database_changed(self, event, ids):
self.need_refresh.emit()
@@ -72,12 +75,87 @@ class TagsView(QTreeView): # {{{
self.recount() # Must happen after the emission of the restriction_set signal
self.tags_marked.emit(self._model.tokens(), self.match_all)
+ def mouseReleaseEvent(self, event):
+ # Swallow everything except leftButton so context menus work correctly
+ if event.button() == Qt.LeftButton:
+ QTreeView.mouseReleaseEvent(self, event)
+
def toggle(self, index):
modifiers = int(QApplication.keyboardModifiers())
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
if self._model.toggle(index, exclusive):
self.tags_marked.emit(self._model.tokens(), self.match_all)
+ def context_menu_handler(self, action=None, category=None):
+ if not action:
+ return
+ try:
+ if action == 'manage_tags':
+ self.tag_list_edit.emit(category)
+ return
+ if action == 'manage_categories':
+ self.user_category_edit.emit(category)
+ return
+ if action == 'manage_searches':
+ self.saved_search_edit.emit(category)
+ return
+ if action == 'hide':
+ self.hidden_categories.add(category)
+ elif action == 'show':
+ self.hidden_categories.discard(category)
+ elif action == 'defaults':
+ self.hidden_categories.clear()
+ config.set('tag_browser_hidden_categories', self.hidden_categories)
+ self.set_new_model()
+ except:
+ return
+
+ def show_context_menu(self, point):
+ index = self.indexAt(point)
+ if not index.isValid():
+ return False
+ item = index.internalPointer()
+ tag_name = ''
+ if item.type == TagTreeItem.TAG:
+ tag_name = item.tag.name
+ item = item.parent
+ if item.type == TagTreeItem.CATEGORY:
+ category = unicode(item.name.toString())
+ self.context_menu = QMenu(self)
+ self.context_menu.addAction(_('Hide %s') % category,
+ partial(self.context_menu_handler, action='hide', category=category))
+
+ if self.hidden_categories:
+ self.context_menu.addSeparator()
+ m = self.context_menu.addMenu(_('Show category'))
+ for col in self.hidden_categories:
+ m.addAction(col,
+ partial(self.context_menu_handler, action='show', category=col))
+ self.context_menu.addSeparator()
+ self.context_menu.addAction(_('Restore defaults'),
+ partial(self.context_menu_handler, action='defaults'))
+
+ self.context_menu.addSeparator()
+ self.context_menu.addAction(_('Manage Tags'),
+ partial(self.context_menu_handler, action='manage_tags',
+ category=tag_name))
+
+ if category in prefs['user_categories'].keys():
+ self.context_menu.addAction(_('Manage User Categories'),
+ partial(self.context_menu_handler, action='manage_categories',
+ category=category))
+ else:
+ self.context_menu.addAction(_('Manage User Categories'),
+ partial(self.context_menu_handler, action='manage_categories',
+ category=None))
+
+ self.context_menu.addAction(_('Manage Saved Searches'),
+ partial(self.context_menu_handler, action='manage_searches',
+ category=tag_name))
+
+ self.context_menu.popup(self.mapToGlobal(point))
+ return True
+
def clear(self):
self.model().clear_state()
@@ -110,13 +188,12 @@ class TagsView(QTreeView): # {{{
self.setCurrentIndex(idx)
self.scrollTo(idx, QTreeView.PositionAtCenter)
- '''
- If the number of user categories changed, or if custom columns have come or gone,
- we must rebuild the model. Reason: it is much easier to do that than to reconstruct
- the browser tree.
- '''
+ # If the number of user categories changed, if custom columns have come or
+ # gone, or if columns have been hidden or restored, we must rebuild the
+ # model. Reason: it is much easier than reconstructing the browser tree.
def set_new_model(self):
- self._model = TagsModel(self.db, parent=self)
+ self._model = TagsModel(self.db, parent=self,
+ hidden_categories=self.hidden_categories)
self.setModel(self._model)
# }}}
@@ -200,7 +277,7 @@ class TagTreeItem(object): # {{{
class TagsModel(QAbstractItemModel): # {{{
- def __init__(self, db, parent=None):
+ def __init__(self, db, parent=None, hidden_categories=None):
QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication
@@ -220,6 +297,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db
+ self.hidden_categories = hidden_categories
self.search_restriction = ''
self.ignore_next_search = 0
@@ -237,6 +315,8 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.get_node_tree(config['sort_by_popularity'])
self.root_item = TagTreeItem()
for i, r in enumerate(self.row_map):
+ if self.hidden_categories and self.categories[i] in self.hidden_categories:
+ continue
if self.db.field_metadata[r]['kind'] != 'user':
tt = _('The lookup/search name is "{0}"').format(r)
else:
@@ -271,12 +351,16 @@ class TagsModel(QAbstractItemModel): # {{{
def refresh(self):
data = self.get_node_tree(config['sort_by_popularity']) # get category data
+ row_index = -1
for i, r in enumerate(self.row_map):
- category = self.root_item.children[i]
+ if self.hidden_categories and self.categories[i] in self.hidden_categories:
+ continue
+ row_index += 1
+ category = self.root_item.children[row_index]
names = [t.tag.name for t in category.children]
states = [t.tag.state for t in category.children]
state_map = dict(izip(names, states))
- category_index = self.index(i, 0, QModelIndex())
+ category_index = self.index(row_index, 0, QModelIndex())
if len(category.children) > 0:
self.beginRemoveRows(category_index, 0,
len(category.children)-1)
@@ -401,16 +485,20 @@ class TagsModel(QAbstractItemModel): # {{{
def tokens(self):
ans = []
tags_seen = set()
+ row_index = -1
for i, key in enumerate(self.row_map):
+ if self.hidden_categories and self.categories[i] in self.hidden_categories:
+ continue
+ row_index += 1
if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category
continue
- category_item = self.root_item.children[i]
+ category_item = self.root_item.children[row_index]
for tag_item in category_item.children:
tag = tag_item.tag
if tag.state > 0:
prefix = ' not ' if tag.state == 2 else ''
category = key if key != 'news' else 'tag'
- if tag.name[0] == u'\u2605': # char is a star. Assume rating
+ if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
else:
if category == 'tags':
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py
index 5f7d4b76cd..773f44acd2 100644
--- a/src/calibre/gui2/ui.py
+++ b/src/calibre/gui2/ui.py
@@ -61,6 +61,8 @@ from calibre.library.database2 import LibraryDatabase2
from calibre.library.caches import CoverCache
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories
+from calibre.gui2.dialogs.tag_list_editor import TagListEditor
+from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
class SaveMenu(QMenu):
@@ -542,19 +544,23 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.cover_cache = CoverCache(self.library_path)
self.cover_cache.start()
self.library_view.model().cover_cache = self.cover_cache
- self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_edit_categories)
+ self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_user_categories_edit)
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
self.tags_view.tags_marked.connect(self.search.search_from_tags)
for x in (self.saved_search.clear_to_help, self.mark_restriction_set):
self.tags_view.restriction_set.connect(x)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
+ self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
+ self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
+ self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
self.search.search.connect(self.tags_view.model().reinit)
for x in (self.location_view.count_changed, self.tags_view.recount,
self.restriction_count_changed):
self.library_view.model().count_changed_signal.connect(x)
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
- self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.saved_searches_changed, Qt.QueuedConnection)
+ self.connect(self.saved_search, SIGNAL('changed()'),
+ self.tags_view.saved_searches_changed, Qt.QueuedConnection)
if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@@ -647,13 +653,28 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self._add_filesystem_book = Dispatcher(self.__add_filesystem_book)
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
- def do_edit_categories(self):
- d = TagCategories(self, self.library_view.model().db)
+ def do_user_categories_edit(self, on_category=None):
+ d = TagCategories(self, self.library_view.model().db, on_category)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.set_new_model()
self.tags_view.recount()
+ def do_tags_list_edit(self, tag):
+ d = TagListEditor(self, self.library_view.model().db, tag)
+ d.exec_()
+ if d.result() == d.Accepted:
+ self.tags_view.set_new_model()
+ self.tags_view.recount()
+ self.library_view.model().refresh()
+
+ def do_saved_search_edit(self, search):
+ d = SavedSearchEditor(self, search)
+ d.exec_()
+ if d.result() == d.Accepted:
+ self.tags_view.saved_searches_changed(recount=True)
+ self.saved_search.clear_to_help()
+
def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev)
self.search.setMaximumWidth(self.width()-150)
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index f27a42beee..0544293095 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -648,6 +648,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute(st%dict(ltable='publishers', table='publishers', ltable_col='publisher'))
self.conn.execute(st%dict(ltable='tags', table='tags', ltable_col='tag'))
self.conn.execute(st%dict(ltable='series', table='series', ltable_col='series'))
+ for id_, tag in self.conn.get('SELECT id, name FROM tags', all=True):
+ if not tag.strip():
+ self.conn.execute('DELETE FROM books_tags_link WHERE tag=?',
+ (id_,))
+ self.conn.execute('DELETE FROM tags WHERE id=?', (id_,))
self.clean_custom()
self.conn.commit()
@@ -980,6 +985,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if notify:
self.notify('metadata', [id])
+ # Convenience method for tags_list_editor
+ def get_tags_with_ids(self):
+ result = self.conn.get('SELECT * FROM tags')
+ if not result:
+ return {}
+ r = []
+ for k,v in result:
+ r.append((k,v))
+ return r
+
+ def rename_tag(self, id, new):
+ self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new, id))
+ self.conn.commit()
+
def get_tags(self, id):
result = self.conn.get(
'SELECT name FROM tags WHERE id IN (SELECT tag FROM books_tags_link WHERE book=?)',
diff --git a/src/calibre/manual/news_recipe.rst b/src/calibre/manual/news_recipe.rst
index 8f39cee387..c840cefb53 100644
--- a/src/calibre/manual/news_recipe.rst
+++ b/src/calibre/manual/news_recipe.rst
@@ -115,7 +115,7 @@ Pre/post processing of downloaded HTML
.. automethod:: BasicNewsRecipe.postprocess_html
-
+.. automethod:: BasicNewsRecipe.populate_article_metadata
Convenience methods
diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py
index 9faabb2615..79cec4a2a7 100644
--- a/src/calibre/web/feeds/news.py
+++ b/src/calibre/web/feeds/news.py
@@ -254,7 +254,7 @@ class BasicNewsRecipe(Recipe):
#: will remove everythong from `` to `