KG revisions

This commit is contained in:
GRiker 2010-06-04 07:16:27 -06:00
commit 0086263f52
20 changed files with 450 additions and 127 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 B

After

Width:  |  Height:  |  Size: 820 B

View File

@ -5,7 +5,6 @@ __copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
clarin.com
'''
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
class Clarin(BasicNewsRecipe):
@ -18,11 +17,12 @@ class Clarin(BasicNewsRecipe):
max_articles_per_feed = 100
use_embedded_content = False
no_stylesheets = True
cover_url = strftime('http://www.clarin.com/diario/%Y/%m/%d/portada.jpg')
encoding = 'cp1252'
language = 'es'
masthead_url = 'http://www.clarin.com/shared/v10/img/Hd/lg_Clarin.gif'
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,"Times New Roman",Times,serif; font-size: xx-large} .Volan,.Pie,.Autor{ font-size: x-small} .Copete,.Hora{font-size: large} '
encoding = 'utf8'
language = 'es_AR'
publication_type = 'newspaper'
INDEX = 'http://www.clarin.com'
masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg'
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,serif; font-size: xx-large} .hora{font-weight:bold} .hd p{font-size: small} .nombre-autor{color: #0F325A} '
conversion_options = {
'comment' : description
@ -31,27 +31,32 @@ class Clarin(BasicNewsRecipe):
, 'language' : language
}
remove_tags = [
dict(name='a' , attrs={'class':'Imp' })
,dict(name='div' , attrs={'class':'Perma' })
,dict(name='h1' , text='Imprimir' )
]
keep_only_tags = [dict(attrs={'class':['hd','mt']})]
feeds = [
(u'Ultimo Momento', u'http://www.clarin.com/diario/hoy/um/sumariorss.xml')
,(u'El Pais' , u'http://www.clarin.com/diario/hoy/elpais.xml' )
,(u'Opinion' , u'http://www.clarin.com/diario/hoy/opinion.xml' )
,(u'El Mundo' , u'http://www.clarin.com/diario/hoy/elmundo.xml' )
,(u'Sociedad' , u'http://www.clarin.com/diario/hoy/sociedad.xml' )
,(u'La Ciudad' , u'http://www.clarin.com/diario/hoy/laciudad.xml' )
,(u'Policiales' , u'http://www.clarin.com/diario/hoy/policiales.xml' )
,(u'Deportes' , u'http://www.clarin.com/diario/hoy/deportes.xml' )
(u'Pagina principal', u'http://www.clarin.com/rss/' )
,(u'Politica' , u'http://www.clarin.com/rss/politica/' )
,(u'Deportes' , u'http://www.clarin.com/rss/deportes/' )
,(u'Economia' , u'http://www.clarin.com/economia/' )
,(u'Mundo' , u'http://www.clarin.com/rss/mundo/' )
,(u'Espectaculos' , u'http://www.clarin.com/rss/espectaculos/')
,(u'Sociedad' , u'http://www.clarin.com/rss/sociedad/' )
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
,(u'Policiales' , u'http://www.clarin.com/rss/policiales/' )
,(u'Internet' , u'http://www.clarin.com/rss/internet/' )
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
]
def print_version(self, url):
rest = url.partition('-0')[-1]
lmain = rest.partition('.')[0]
lurl = u'http://www.servicios.clarin.com/notas/jsp/clarin/v9/notas/imprimir.jsp?pagid=' + lmain
return lurl
return url + '?print=1'
def get_cover_url(self):
cover_url = None
soup = self.index_to_soup(self.INDEX)
cover_item = soup.find('div',attrs={'class':'bb-md bb-md-edicion_papel'})
if cover_item:
ap = cover_item.find('a',attrs={'href':'/edicion-impresa/'})
if ap:
cover_url = self.INDEX + ap.img['src']
return cover_url

View File

@ -21,12 +21,16 @@ class weltDe(BasicNewsRecipe):
no_stylesheets = True
remove_stylesheets = True
remove_javascript = True
encoding = 'iso-8859-1'
BasicNewsRecipe.summary_length = 200
encoding = 'utf-8'
html2epub_options = 'linearize_tables = True\nbase_font_size2=10'
BasicNewsRecipe.summary_length = 100
remove_tags = [dict(id='jumplinks'),
dict(id='ad1'),
dict(id='top'),
dict(id='header'),
dict(id='additionalNavWrapper'),
dict(id='fullimage_index'),
dict(id='additionalNav'),
dict(id='printMenu'),
@ -35,6 +39,8 @@ class weltDe(BasicNewsRecipe):
dict(id='servicesBox'),
dict(id='servicesNav'),
dict(id='ad2'),
dict(id='banner_1'),
dict(id='ssoInfoTop'),
dict(id='brandingWrapper'),
dict(id='links-intern'),
dict(id='navigation'),
@ -53,10 +59,22 @@ class weltDe(BasicNewsRecipe):
dict(id='xmsg_comment'),
dict(id='additionalNavWrapper'),
dict(id='imagebox'),
dict(id='footerContainer'),
#dict(id=''),
dict(name='span'),
dict(name='div', attrs={'class':'printURL'}),
dict(name='ul', attrs={'class':'clear mainNavigation inline'}),
dict(name='ul', attrs={'class':'inline'}),
dict(name='ul', attrs={'class':'ubar'}),
dict(name='hr', attrs={'class':'ubar'}),
dict(name='li', attrs={'class':'counter'}),
dict(name='li', attrs={'class':'browseBack'}),
dict(name='li', attrs={'class':'browseNext'}),
dict(name='li', attrs={'class':'selected'}),
dict(name='div', attrs={'class':'floatLeft'}),
dict(name='div', attrs={'class':'ad'}),
dict(name='div', attrs={'class':'ftBarLeft'}),
dict(name='div', attrs={'class':'clear additionalNav'}),
dict(name='div', attrs={'class':'inlineBox inlineFurtherLinks'}),
dict(name='div', attrs={'class':'inlineBox videoInlineBox'}),
dict(name='div', attrs={'class':'inlineGallery'}),
@ -65,6 +83,23 @@ class weltDe(BasicNewsRecipe):
dict(name='div', attrs={'class':'articleOptions clear'}),
dict(name='div', attrs={'class':'noPrint galleryIndex'}),
dict(name='div', attrs={'class':'inlineBox inlineTagCloud'}),
dict(name='div', attrs={'class':'clear module writeComment bgColor1'}),
dict(name='div', attrs={'class':'clear module textGallery bgColor1'}),
dict(name='div', attrs={'class':'clear module socialMedia bgColor1'}),
dict(name='div', attrs={'class':'clear module continuativeLinks'}),
dict(name='div', attrs={'class':'moreArtH3'}),
dict(name='div', attrs={'class':'jqmWindow'}),
dict(name='div', attrs={'class':'clear gap4'}),
dict(name='div', attrs={'class':'hidden'}),
dict(name='div', attrs={'class':'advertising'}),
dict(name='div', attrs={'class':'ad adMarginBottom'}),
dict(name='div', attrs={'class':'ad'}),
dict(name='div', attrs={'class':'topLine'}),
dict(name='div', attrs={'class':'toplineH2'}),
dict(name='div', attrs={'class':'headLineH3'}),
dict(name='div', attrs={'class':'print'}),
dict(name='div', attrs={'class':'clear menu'}),
dict(name='div', attrs={'class':'clear galleryContent'}),
dict(name='p', attrs={'class':'jump'}),
dict(name='a', attrs={'class':'commentLink'}),
dict(name='h2', attrs={'class':'jumpHeading'}),
@ -75,7 +110,7 @@ class weltDe(BasicNewsRecipe):
dict(name='table', attrs={'class':'textGallery'}),
dict(name='li', attrs={'class':'active'})]
remove_tags_after = [dict(id='tw_link_widget')]
remove_tags_after = [dict(name='div', attrs={'class':'clear departmentLine'})]
extra_css = '''
h2{font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #003399;}
@ -87,7 +122,6 @@ class weltDe(BasicNewsRecipe):
.photo {font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #666666;} '''
feeds = [ ('Politik', 'http://welt.de/politik/?service=Rss'),
('Deutsche Dinge', 'http://www.welt.de/deutsche-dinge/?service=Rss'),
('Wirtschaft', 'http://welt.de/wirtschaft/?service=Rss'),
('Finanzen', 'http://welt.de/finanzen/?service=Rss'),
('Sport', 'http://welt.de/sport/?service=Rss'),
@ -101,4 +135,5 @@ class weltDe(BasicNewsRecipe):
def print_version(self, url):
return url.replace ('.html', '.html?print=yes')
return url.replace ('.html', '.html?print=true')

View File

@ -1334,7 +1334,7 @@ class MobiWriter(object):
item = self._oeb.manifest.hrefs[href]
try:
data = rescale_image(item.data, self._imagemax)
except IOError:
except:
self._oeb.logger.warn('Bad image file %r' % item.href)
continue
self._records.append(data)

View File

@ -222,6 +222,8 @@ class DBAdder(Thread):
class Adder(QObject):
ADD_TIMEOUT = 600 # seconds
def __init__(self, parent, db, callback, spare_server=None):
QObject.__init__(self, parent)
self.pd = ProgressDialog(_('Adding...'), parent=parent)
@ -328,7 +330,7 @@ class Adder(QObject):
except Empty:
pass
if (time.time() - self.last_added_at) > 300:
if (time.time() - self.last_added_at) > self.ADD_TIMEOUT:
self.timer.stop()
self.pd.hide()
self.db_adder.end = True

View File

@ -3,14 +3,12 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
from PyQt4.QtCore import SIGNAL
from PyQt4.QtGui import QDialog
from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor
from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.constants import islinux
class SavedSearchEditor(QDialog, Ui_SavedSearchEditor):

View File

@ -1,17 +1,17 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from functools import partial
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
from calibre.ebooks.metadata import title_sort
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):
def __init__(self, window, db, tag_to_match, category):
QDialog.__init__(self, window)
Ui_TagListEditor.__init__(self)
self.setupUi(self)
@ -20,9 +20,28 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.to_delete = []
self.db = db
self.all_tags = {}
for k,v in db.get_tags_with_ids():
self.category = category
if category == 'tags':
result = db.get_tags_with_ids()
compare = (lambda x,y:cmp(x.lower(), y.lower()))
elif category == 'series':
result = db.get_series_with_ids()
compare = (lambda x,y:cmp(title_sort(x).lower(), title_sort(y).lower()))
elif category == 'publisher':
result = db.get_publishers_with_ids()
compare = (lambda x,y:cmp(x.lower(), y.lower()))
else: # should be a custom field
self.cc_label = None
if category in db.field_metadata:
self.cc_label = db.field_metadata[category]['label']
result = self.db.get_custom_items_with_ids(label=self.cc_label)
else:
result = []
compare = (lambda x,y:cmp(x.lower(), y.lower()))
for k,v in result:
self.all_tags[v] = k
for tag in sorted(self.all_tags.keys(), cmp=self.tag_cmp):
for tag in sorted(self.all_tags.keys(), cmp=compare):
item = QListWidgetItem(tag)
item.setData(Qt.UserRole, self.all_tags[tag])
self.available_tags.addItem(item)
@ -37,13 +56,18 @@ class TagListEditor(QDialog, Ui_TagListEditor):
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_()
if not item.text():
error_dialog(self, _('Item is blank'),
_('An item cannot be set to nothing. Delete it instead.')).exec_()
item.setText(self.item_before_editing.text())
return
id,ign = self.item_before_editing.data(Qt.UserRole).toInt()
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, _('Item already used'),
_('The item %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):
@ -52,38 +76,53 @@ class TagListEditor(QDialog, Ui_TagListEditor):
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_()
error_dialog(self, _('No item selected'),
_('You must select one item from the list of Available items.')).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_()
deletes = self.available_tags.selectedItems() if item is None else [item]
if not deletes:
error_dialog(self, _('No items selected'),
_('You must select at least one items from the list.')).exec_()
return
ct = ', '.join([unicode(item.text()) for item in deletes])
if not question_dialog(self, _('Are your sure?'),
'<p>'+_('Are you certain you want to delete the following items?')+'<br>'+ct):
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?'),
'<p>'+_('The following tags are used by one or more books. '
'Are you certain you want to delete them?')+'<br>'+ct):
deletes += confirms
for item in deletes:
self.to_delete.append(item)
(id,ign) = item.data(Qt.UserRole).toInt()
self.to_delete.append(id)
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)
rename_func = None
if self.category == 'tags':
rename_func = self.db.rename_tag
delete_func = self.db.delete_tag_using_id
elif self.category == 'series':
rename_func = self.db.rename_series
delete_func = self.db.delete_series_using_id
elif self.category == 'publisher':
rename_func = self.db.rename_publisher
delete_func = self.db.delete_publisher_using_id
else:
rename_func = partial(self.db.rename_custom_item, label=self.cc_label)
delete_func = partial(self.db.delete_custom_item_using_id, label=self.cc_label)
work_done = False
if rename_func:
for text in self.to_rename:
work_done = True
rename_func(id=self.to_rename[text], new_name=unicode(text))
for item in self.to_delete:
work_done = True
delete_func(item)
if not work_done:
QDialog.reject(self)
else:
QDialog.accept(self)

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Tag Editor</string>
<string>Category Editor</string>
</property>
<property name="windowIcon">
<iconset>
@ -25,7 +25,7 @@
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Tags in use</string>
<string>Items in use</string>
</property>
<property name="buddy">
<cstring>available_tags</cstring>
@ -54,7 +54,7 @@
<item>
<widget class="QToolButton" name="delete_button">
<property name="toolTip">
<string>Delete tag from database. This will unapply the tag from all books and then remove it from the database.</string>
<string>Delete item from database. This will unapply the item from all books and then remove it from the database.</string>
</property>
<property name="text">
<string>...</string>
@ -74,7 +74,7 @@
<item>
<widget class="QToolButton" name="rename_button">
<property name="toolTip">
<string>Rename the tag everywhere it is used.</string>
<string>Rename the item in every book where it is used.</string>
</property>
<property name="text">
<string>...</string>

View File

@ -75,6 +75,9 @@ class BooksView(QTableView): # {{{
h.setSectionHidden(idx, True)
elif action == 'show':
h.setSectionHidden(idx, False)
if h.sectionSize(idx) < 3:
sz = h.sectionSizeHint(idx)
h.resizeSection(idx, sz)
elif action == 'ascending':
self.sortByColumn(idx, Qt.AscendingOrder)
elif action == 'descending':
@ -257,6 +260,11 @@ class BooksView(QTableView): # {{{
for col, alignment in state.get('column_alignment', {}).items():
self._model.change_alignment(col, alignment)
for i in range(h.count()):
if not h.isSectionHidden(i) and h.sectionSize(i) < 3:
sz = h.sectionSizeHint(i)
h.resizeSection(i, sz)
def get_default_state(self):
old_state = {
'hidden_columns': [],

View File

@ -17,15 +17,18 @@ from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog
class TagsView(QTreeView): # {{{
need_refresh = pyqtSignal()
refresh_required = pyqtSignal()
restriction_set = pyqtSignal(object)
tags_marked = pyqtSignal(object, object)
user_category_edit = pyqtSignal(object)
tag_list_edit = pyqtSignal(object)
tag_list_edit = pyqtSignal(object, object)
saved_search_edit = pyqtSignal(object)
tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal()
def __init__(self, *args):
QTreeView.__init__(self, *args)
@ -36,7 +39,8 @@ class TagsView(QTreeView): # {{{
def set_database(self, db, tag_match, popularity, restriction):
self.hidden_categories = config['tag_browser_hidden_categories']
self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories)
self._model = TagsModel(db, parent=self,
hidden_categories=self.hidden_categories)
self.popularity = popularity
self.restriction = restriction
self.tag_match = tag_match
@ -48,12 +52,12 @@ class TagsView(QTreeView): # {{{
self.popularity.setChecked(config['sort_by_popularity'])
self.popularity.stateChanged.connect(self.sort_changed)
self.restriction.activated[str].connect(self.search_restriction_set)
self.need_refresh.connect(self.recount, type=Qt.QueuedConnection)
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
db.add_listener(self.database_changed)
self.saved_searches_changed(recount=False)
def database_changed(self, event, ids):
self.need_refresh.emit()
self.refresh_required.emit()
@property
def match_all(self):
@ -80,18 +84,26 @@ class TagsView(QTreeView): # {{{
if event.button() == Qt.LeftButton:
QTreeView.mouseReleaseEvent(self, event)
def mouseDoubleClickEvent(self, event):
# swallow these to avoid toggling and editing at the same time
pass
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):
def context_menu_handler(self, action=None, category=None,
key=None, index=None):
if not action:
return
try:
if action == 'manage_tags':
self.tag_list_edit.emit(category)
if action == 'edit_item':
self.edit(index)
return
if action == 'open_editor':
self.tag_list_edit.emit(category, key)
return
if action == 'manage_categories':
self.user_category_edit.emit(category)
@ -117,29 +129,51 @@ class TagsView(QTreeView): # {{{
item = index.internalPointer()
tag_name = ''
if item.type == TagTreeItem.TAG:
tag_item = item
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))
key = item.category_key
# Verify that we are working with a field that we know something about
if key not in self.db.field_metadata:
return True
if self.hidden_categories:
self.context_menu = QMenu(self)
# If the user right-clicked on an editable item, then offer
# the possibility of renaming that item
if tag_name and \
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
self.db.field_metadata[key]['is_custom']):
self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
partial(self.context_menu_handler, action='edit_item',
category=tag_item, index=index))
self.context_menu.addSeparator()
# Hide/Show/Restore categories
self.context_menu.addAction(_('Hide category %s') % category,
partial(self.context_menu_handler, action='hide', category=category))
if self.hidden_categories:
m = self.context_menu.addMenu(_('Show category'))
for col in self.hidden_categories:
for col in sorted(self.hidden_categories, cmp=lambda x,y: cmp(x.lower(), y.lower())):
m.addAction(col,
partial(self.context_menu_handler, action='show', category=col))
self.context_menu.addSeparator()
self.context_menu.addAction(_('Restore defaults'),
self.context_menu.addAction(_('Show all categories'),
partial(self.context_menu_handler, action='defaults'))
# Offer specific editors for tags/series/publishers/saved searches
self.context_menu.addSeparator()
self.context_menu.addAction(_('Manage Tags'),
partial(self.context_menu_handler, action='manage_tags',
category=tag_name))
if key in ['tags', 'publisher', 'series'] or \
self.db.field_metadata[key]['is_custom']:
self.context_menu.addAction(_('Manage ') + category,
partial(self.context_menu_handler, action='open_editor',
category=tag_name, key=key))
elif key == 'search':
self.context_menu.addAction(_('Manage Saved Searches'),
partial(self.context_menu_handler, action='manage_searches',
category=tag_name))
# Always show the user categories editor
self.context_menu.addSeparator()
if category in prefs['user_categories'].keys():
self.context_menu.addAction(_('Manage User Categories'),
partial(self.context_menu_handler, action='manage_categories',
@ -149,10 +183,6 @@ class TagsView(QTreeView): # {{{
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
@ -203,7 +233,8 @@ class TagTreeItem(object): # {{{
TAG = 1
ROOT = 2
def __init__(self, data=None, category_icon=None, icon_map=None, parent=None, tooltip=None):
def __init__(self, data=None, category_icon=None, icon_map=None,
parent=None, tooltip=None, category_key=None):
self.parent = parent
self.children = []
if self.parent is not None:
@ -218,6 +249,7 @@ class TagTreeItem(object): # {{{
self.bold_font = QFont()
self.bold_font.setBold(True)
self.bold_font = QVariant(self.bold_font)
self.category_key = category_key
elif self.type == self.TAG:
icon_map[0] = data.icon
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
@ -263,6 +295,8 @@ class TagTreeItem(object): # {{{
return QVariant('%s'%(self.tag.name))
else:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
if role == Qt.EditRole:
return QVariant(self.tag.name)
if role == Qt.DecorationRole:
return self.icon_state_map[self.tag.state]
if role == Qt.ToolTipRole and self.tag.tooltip is not None:
@ -277,7 +311,7 @@ class TagTreeItem(object): # {{{
class TagsModel(QAbstractItemModel): # {{{
def __init__(self, db, parent=None, hidden_categories=None):
def __init__(self, db, parent, hidden_categories=None):
QAbstractItemModel.__init__(self, parent)
# must do this here because 'QPixmap: Must construct a QApplication
@ -297,6 +331,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db
self.tags_view = parent
self.hidden_categories = hidden_categories
self.search_restriction = ''
self.ignore_next_search = 0
@ -324,7 +359,7 @@ class TagsModel(QAbstractItemModel): # {{{
c = TagTreeItem(parent=self.root_item,
data=self.categories[i],
category_icon=self.category_icon_map[r],
tooltip=tt)
tooltip=tt, category_key=r)
for tag in data[r]:
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
@ -342,8 +377,12 @@ class TagsModel(QAbstractItemModel): # {{{
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
tb_categories = self.db.field_metadata
self.category_items = {}
for category in tb_categories:
if category in data: # They should always be there, but ...
# make a map of sets of names per category for duplicate
# checking when editing
self.category_items[category] = set([tag.name for tag in data[category]])
self.row_map.append(category)
self.categories.append(tb_categories[category]['name'])
@ -382,11 +421,52 @@ class TagsModel(QAbstractItemModel): # {{{
item = index.internalPointer()
return item.data(role)
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return NONE
val = unicode(value.toString())
if not val:
error_dialog(self.tags_view, _('Item is blank'),
_('An item cannot be set to nothing. Delete it instead.')).exec_()
return False
item = index.internalPointer()
key = item.parent.category_key
# make certain we know about the category
if key not in self.db.field_metadata:
return
if val in self.category_items[key]:
error_dialog(self.tags_view, 'Duplicate item',
_('The name %s is already used.')%val).exec_()
return False
oldval = item.tag.name
if key == 'search':
saved_searches.rename(unicode(item.data(role).toString()), val)
self.tags_view.search_item_renamed.emit()
else:
if key == 'series':
self.db.rename_series(item.tag.id, val)
elif key == 'publisher':
self.db.rename_publisher(item.tag.id, val)
elif key == 'tags':
self.db.rename_tag(item.tag.id, val)
elif key == 'authors':
self.db.rename_author(item.tag.id, val)
elif self.db.field_metadata[key]['is_custom']:
self.db.rename_custom_item(item.tag.id, val,
label=self.db.field_metadata[key]['label'])
self.tags_view.tag_item_renamed.emit()
item.tag.name = val
self.dataChanged.emit(index, index)
# replace the old value in the duplicate detection map with the new one
self.category_items[key].discard(oldval)
self.category_items[key].add(val)
return True
def headerData(self, *args):
return NONE
def flags(self, *args):
return Qt.ItemIsEnabled|Qt.ItemIsSelectable
return Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable
def path_for_index(self, index):
ans = []

View File

@ -553,6 +553,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
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.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
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):
@ -660,13 +662,22 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
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)
def do_tags_list_edit(self, tag, category):
d = TagListEditor(self, self.library_view.model().db, tag, category)
d.exec_()
if d.result() == d.Accepted:
# Clean up everything, as information could have changed for many books.
self.library_view.model().refresh()
self.tags_view.set_new_model()
self.tags_view.recount()
self.library_view.model().refresh()
self.saved_search.clear_to_help()
self.search.clear_to_help()
def do_tag_item_renamed(self):
# Clean up library view and search
self.library_view.model().refresh()
self.saved_search.clear_to_help()
self.search.clear_to_help()
def do_saved_search_edit(self, search):
d = SavedSearchEditor(self, search)

View File

@ -171,6 +171,40 @@ class CustomColumns(object):
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
return ans
# convenience methods for tag editing
def get_custom_items_with_ids(self, label=None, num=None):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
table,lt = self.custom_table_names(data['num'])
if not data['normalized']:
return []
ans = self.conn.get('SELECT id, value FROM %s'%table)
return ans
def rename_custom_item(self, id, new_name, label=None, num=None):
if id:
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
table,lt = self.custom_table_names(data['num'])
self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, id))
self.conn.commit()
def delete_custom_item_using_id(self, id, label=None, num=None):
if id:
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
table,lt = self.custom_table_names(data['num'])
self.conn.execute('DELETE FROM %s WHERE value=?'%lt, (id,))
self.conn.execute('DELETE FROM %s WHERE id=?'%table, (id,))
self.conn.commit()
# end convenience methods
def all_custom(self, label=None, num=None):
if label is not None:
data = self.custom_column_label_map[label]

View File

@ -643,11 +643,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
'''
Remove orphaned entries.
'''
st = 'DELETE FROM %(table)s WHERE (SELECT COUNT(id) FROM books_%(ltable)s_link WHERE %(ltable_col)s=%(table)s.id) < 1;'
self.conn.execute(st%dict(ltable='authors', table='authors', ltable_col='author'))
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'))
def doit(ltable, table, ltable_col):
st = ('DELETE FROM books_%s_link WHERE (SELECT COUNT(id) '
'FROM books WHERE id=book) < 1;')%ltable
self.conn.execute(st)
st = ('DELETE FROM %(table)s WHERE (SELECT COUNT(id) '
'FROM books_%(ltable)s_link WHERE '
'%(ltable_col)s=%(table)s.id) < 1;') % dict(
ltable=ltable, table=table, ltable_col=ltable_col)
self.conn.execute(st)
for ltable, table, ltable_col in [
('authors', 'authors', 'author'),
('publishers', 'publishers', 'publisher'),
('tags', 'tags', 'tag'),
('series', 'series', 'series')
]:
doit(ltable, table, ltable_col)
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=?',
@ -730,9 +743,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
icon=icon, tooltip = tooltip)
for r in data if item_not_zero_func(r)]
if category == 'series':
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name),
title_sort(y.name)))
if category == 'series' and not sort_on_count:
categories[category].sort(cmp=lambda x,y:cmp(title_sort(x.name).lower(),
title_sort(y.name).lower()))
# We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically
@ -985,19 +998,91 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if notify:
self.notify('metadata', [id])
# Convenience method for tags_list_editor
# Convenience methods for tags_list_editor
# Note: we generally do not need to refresh_ids because library_view will
# refresh everything.
def get_tags_with_ids(self):
result = self.conn.get('SELECT * FROM tags')
result = self.conn.get('SELECT id,name FROM tags')
if not result:
return {}
r = []
for k,v in result:
r.append((k,v))
return r
return []
return result
def rename_tag(self, id, new):
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new, id))
self.conn.commit()
def rename_tag(self, id, new_name):
if id:
self.conn.execute('UPDATE tags SET name=? WHERE id=?', (new_name, id))
self.conn.commit()
def delete_tag_using_id(self, id):
if id:
self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,))
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
self.conn.commit()
def get_series_with_ids(self):
result = self.conn.get('SELECT id,name FROM series')
if not result:
return []
return result
def rename_series(self, id, new_name):
if id:
self.conn.execute('UPDATE series SET name=? WHERE id=?', (new_name, id))
self.conn.commit()
def delete_series_using_id(self, id):
if id:
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,))
self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,))
self.conn.execute('DELETE FROM series WHERE id=?', (id,))
self.conn.commit()
for (book_id,) in books:
self.conn.execute('UPDATE books SET series_index=1.0 WHERE id=?', (book_id,))
def get_publishers_with_ids(self):
result = self.conn.get('SELECT id,name FROM publishers')
if not result:
return []
return result
def rename_publisher(self, id, new_name):
if id:
self.conn.execute('UPDATE publishers SET name=? WHERE id=?', (new_name, id))
self.conn.commit()
def delete_publisher_using_id(self, id):
if id:
self.conn.execute('DELETE FROM books_publishers_link WHERE publisher=?', (id,))
self.conn.execute('DELETE FROM publishers WHERE id=?', (id,))
self.conn.commit()
# There is no editor for author, so we do not need get_authors_with_ids or
# delete_author_using_id.
def rename_author(self, id, new_name):
if id:
# Make sure that any commas in new_name are changed to '|'!
new_name = new_name.replace(',', '|')
self.conn.execute('UPDATE authors SET name=? WHERE id=?', (new_name, id))
self.conn.commit()
# now must fix up the books
books = self.conn.get('SELECT book from books_authors_link WHERE author=?', (id,))
for (book_id,) in books:
# First, must refresh the cache to see the new authors
self.data.refresh_ids(self, [book_id])
# now fix the filesystem paths
self.set_path(book_id, index_is_id=True)
# Next fix the author sort. Reset it to the default
authors = self.conn.get('''
SELECT authors.name
FROM authors, books_authors_link as bl
WHERE bl.book = ? and bl.author = authors.id
''' , (book_id,))
# unpack the double-list structure
for i,aut in enumerate(authors):
authors[i] = aut[0]
ss = authors_to_sort_string(authors)
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id))
# end convenience methods
def get_tags(self, id):
result = self.conn.get(
@ -1083,7 +1168,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('DELETE FROM tags WHERE id=?', (id,))
self.conn.commit()
def set_series(self, id, series, notify=True):
self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,))
self.conn.execute('DELETE FROM series WHERE (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) < 1')
@ -1603,6 +1687,7 @@ books_series_link feeds
def check_integrity(self, callback):
callback(0., _('Checking SQL integrity...'))
self.clean()
user_version = self.user_version
sql = '\n'.join(self.conn.dump())
self.conn.close()

View File

@ -195,11 +195,11 @@ class FieldMetadata(dict):
'is_category':False}),
('ondevice', {'table':None,
'column':None,
'datatype':'bool',
'datatype':'text',
'is_multiple':None,
'kind':'field',
'name':None,
'search_terms':[],
'search_terms':['ondevice'],
'is_custom':False,
'is_category':False}),
('path', {'table':None,

View File

@ -241,9 +241,9 @@ Now, you can access your saved search in the Tag Browser under "Searches". A sin
.. _configuration:
Configuration
Preferences
---------------
The configuration dialog allows you to set some global defaults used by all of |app|. To access it, click the |cbi|.
The Preferences dialog allows you to set some global defaults used by all of |app|. To access it, click the |cbi|.
.. |cbi| image:: images/configuration.png

View File

@ -111,6 +111,8 @@ Pre/post processing of downloaded HTML
.. automember:: BasicNewsRecipe.remove_javascript
.. automethod:: BasicNewsRecipe.prepreprocess_html
.. automethod:: BasicNewsRecipe.preprocess_html
.. automethod:: BasicNewsRecipe.postprocess_html

View File

@ -52,6 +52,12 @@ class SavedSearchQueries(object):
self.queries.pop(self.force_unicode(name), False)
prefs[self.opt_name] = self.queries
def rename(self, old_name, new_name):
self.queries[self.force_unicode(new_name)] = \
self.queries.get(self.force_unicode(old_name), None)
self.queries.pop(self.force_unicode(old_name), False)
prefs[self.opt_name] = self.queries
def names(self):
return sorted(self.queries.keys(),
cmp=lambda x,y: cmp(x.lower(), y.lower()))

View File

@ -412,10 +412,25 @@ class BasicNewsRecipe(Recipe):
return url
return article.get('link', None)
def prepreprocess_html(self, soup):
'''
This method is called with the source of each downloaded :term:`HTML` file, before
any of the cleanup attributes like remove_tags, keep_only_tags are
applied. Note that preprocess_regexps will have already been applied.
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
It should return `soup` after processing it.
`soup`: A `BeautifulSoup <http://www.crummy.com/software/BeautifulSoup/documentation.html>`_
instance containing the downloaded :term:`HTML`.
'''
return soup
def preprocess_html(self, soup):
'''
This method is called with the source of each downloaded :term:`HTML` file, before
it is parsed for links and images.
it is parsed for links and images. It is called after the cleanup as
specified by remove_tags etc.
It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`.
It should return `soup` after processing it.
@ -532,7 +547,7 @@ class BasicNewsRecipe(Recipe):
Intended to be used to get article metadata like author/summary/etc.
from the parsed HTML (soup).
:param article: A object of class :class:`calibre.web.feeds.Article`.
If you change the sumamry, remember to also change the
If you change the summary, remember to also change the
text_summary
:param soup: Parsed HTML belonging to this article
:param first: True iff the parsed HTML is the first page of the article.
@ -612,7 +627,7 @@ class BasicNewsRecipe(Recipe):
self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0]
for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps',
'preprocess_html', 'remove_tags_after',
'prepreprocess_html', 'preprocess_html', 'remove_tags_after',
'remove_tags_before', 'is_link_wanted'):
setattr(self.web2disk_options, extra, getattr(self, extra))
self.web2disk_options.postprocess_html = self._postprocess_html

View File

@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from lxml import html, etree
from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \
STRONG, BR, H1, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \
STRONG, BR, SPAN, A, HR, UL, LI, H2, IMG, P as PT, \
TABLE, TD, TR
from calibre import preferred_encoding, strftime, isbytestring

View File

@ -136,6 +136,7 @@ class RecursiveFetcher(object):
self.remove_tags_before = getattr(options, 'remove_tags_before', None)
self.keep_only_tags = getattr(options, 'keep_only_tags', [])
self.preprocess_html_ext = getattr(options, 'preprocess_html', lambda soup: soup)
self.prepreprocess_html_ext = getattr(options, 'prepreprocess_html', lambda soup: soup)
self.postprocess_html_ext= getattr(options, 'postprocess_html', None)
self._is_link_wanted = getattr(options, 'is_link_wanted',
default_is_link_wanted)
@ -153,6 +154,8 @@ class RecursiveFetcher(object):
nmassage.append((re.compile(r'<!--.*?-->', re.DOTALL), lambda m: ''))
soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage)
soup = self.prepreprocess_html_ext(soup)
if self.keep_only_tags:
body = Tag(soup, 'body')
try: