KG updates

This commit is contained in:
GRiker 2010-06-15 07:54:14 -06:00
commit c056b33814
20 changed files with 549 additions and 75 deletions

View File

@ -71,3 +71,10 @@ gui_pubdate_display_format = 'MMM yyyy'
# order until the title is edited. Double-clicking on a title and hitting return
# without changing anything is sufficient to change the sort.
title_series_sorting = 'library_order'
# How to render average rating in the tag browser.
# There are two rendering methods available. The first is to show a partial
# star, and the second is to show a partially filled rectangle. The first is
# better looking, but uses more screen space than the second.
# Values are 'star' or 'rectangle'
render_avg_rating_using='star'

View File

@ -30,7 +30,7 @@ def authors_to_string(authors):
def author_to_author_sort(author):
method = tweaks['author_sort_copy_method']
if method == 'copy' or (method == 'comma' and author.count(',') > 0):
if method == 'copy' or (method == 'comma' and ',' in author):
return author
tokens = author.split()
tokens = tokens[-1:] + tokens[:-1]

View File

@ -741,7 +741,7 @@ class OPF(object):
def fset(self, val):
for tag in list(self.tags_path(self.metadata)):
self.metadata.remove(tag)
tag.getparent().remove(tag)
for tag in val:
elem = self.create_metadata_element('subject')
self.set_text(elem, unicode(tag))

View File

@ -101,6 +101,8 @@ def _config():
help=_('tag browser categories not to display'))
c.add_opt('gui_layout', choices=['wide', 'narrow'],
help=_('The layout of the user interface'), default='wide')
c.add_opt('show_avg_rating', default=True,
help=_('Show the average rating per item indication in the tag browser'))
return ConfigProxy(c)
config = _config()

View File

@ -13,7 +13,7 @@ from PyQt4.Qt import QPixmap, SIGNAL
from calibre.gui2 import choose_images, error_dialog
from calibre.gui2.convert.metadata_ui import Ui_Form
from calibre.ebooks.metadata import authors_to_string, string_to_authors, \
MetaInformation, authors_to_sort_string
MetaInformation
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2.convert import Widget
@ -57,7 +57,7 @@ class MetadataWidget(Widget, Ui_Form):
au = unicode(self.author.currentText())
au = re.sub(r'\s+et al\.$', '', au)
authors = string_to_authors(au)
self.author_sort.setText(authors_to_sort_string(authors))
self.author_sort.setText(self.db.author_sort_from_authors(authors))
def initialize_metadata_options(self):

View File

@ -23,7 +23,7 @@ from calibre.devices.scanner import DeviceScanner
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
pixmap_to_data, warning_dialog, \
question_dialog, info_dialog, choose_dir
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
from calibre.ebooks.metadata import authors_to_string
from calibre import preferred_encoding, prints
from calibre.utils.filenames import ascii_filename
from calibre.devices.errors import FreeSpaceError
@ -1409,7 +1409,7 @@ class DeviceMixin(object): # {{{
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:
book.author_sort = authors_to_sort_string(book.authors)
book.author_sort = self.db.author_sort_from_authors(book.authors)
resend_metadata = True
if resend_metadata:

View File

@ -481,6 +481,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
self.device_detection_button.clicked.connect(self.debug_device_detection)
self.port.editingFinished.connect(self.check_port_value)
self.search_as_you_type.setChecked(config['search_as_you_type'])
self.show_avg_rating.setChecked(config['show_avg_rating'])
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
True))
li = None
@ -862,6 +864,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked()
config['search_as_you_type'] = self.search_as_you_type.isChecked()
config['show_avg_rating'] = self.show_avg_rating.isChecked()
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())

View File

@ -371,6 +371,16 @@
</widget>
</item>
<item row="5" column="0">
<widget class="QCheckBox" name="show_avg_rating">
<property name="text">
<string>Show &amp;average ratings in the tags browser</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="search_as_you_type">
<property name="text">
<string>Search as you type</string>
@ -380,21 +390,21 @@
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news">
<property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news">
<property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<item row="9" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_6">
@ -411,7 +421,7 @@
</item>
</layout>
</item>
<item row="9" column="0" colspan="2">
<item row="10" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Toolbar</string>
@ -459,7 +469,7 @@
</layout>
</widget>
</item>
<item row="10" column="0" colspan="2">
<item row="11" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QGroupBox" name="groupBox">

View File

@ -0,0 +1,82 @@
#!/usr/bin/env python
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView
from calibre.ebooks.metadata import author_to_author_sort
from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog
class tableItem(QTableWidgetItem):
def __ge__(self, other):
return unicode(self.text()).lower() >= unicode(other.text()).lower()
def __lt__(self, other):
return unicode(self.text()).lower() < unicode(other.text()).lower()
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
def __init__(self, parent, db, id_to_select):
QDialog.__init__(self, parent)
Ui_EditAuthorsDialog.__init__(self)
self.setupUi(self)
self.buttonBox.accepted.connect(self.accepted)
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
self.table.setColumnCount(2)
self.table.setHorizontalHeaderLabels([_('Author'), _('Author sort')])
self.authors = {}
auts = db.get_authors_with_ids()
self.table.setRowCount(len(auts))
select_item = None
for row, (id, author, sort) in enumerate(auts):
author = author.replace('|', ',')
self.authors[id] = (author, sort)
aut = tableItem(author)
aut.setData(Qt.UserRole, id)
sort = tableItem(sort)
self.table.setItem(row, 0, aut)
self.table.setItem(row, 1, sort)
if id == id_to_select:
select_item = sort
self.table.resizeColumnsToContents()
# set up the signal after the table is filled
self.table.cellChanged.connect(self.cell_changed)
self.table.setSortingEnabled(True)
self.table.sortByColumn(1, Qt.AscendingOrder)
if select_item is not None:
self.table.setCurrentItem(select_item)
self.table.editItem(select_item)
else:
self.table.setCurrentCell(0, 0)
def accepted(self):
self.result = []
for row in range(0,self.table.rowCount()):
id = self.table.item(row, 0).data(Qt.UserRole).toInt()[0]
aut = unicode(self.table.item(row, 0).text()).strip()
sort = unicode(self.table.item(row, 1).text()).strip()
orig_aut,orig_sort = self.authors[id]
if orig_aut != aut or orig_sort != sort:
self.result.append((id, orig_aut, aut, sort))
def cell_changed(self, row, col):
if col == 0:
item = self.table.item(row, 0)
aut = unicode(item.text()).strip()
c = self.table.item(row, 1)
c.setText(author_to_author_sort(aut))
item = c
else:
item = self.table.item(row, 1)
self.table.setCurrentItem(item)
# disable and reenable sorting to force the sort now, so we can scroll
# to the item after it moves
self.table.setSortingEnabled(False)
self.table.setSortingEnabled(True)
self.table.scrollToItem(item)

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EditAuthorsDialog</class>
<widget class="QDialog" name="EditAuthorsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>730</width>
<height>342</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Manage authors</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTableWidget" name="table">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="columnCount">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>EditAuthorsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>229</x>
<y>211</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>234</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>EditAuthorsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>297</x>
<y>217</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>234</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -8,7 +8,7 @@ from PyQt4.QtGui import QDialog, QGridLayout
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string, \
from calibre.ebooks.metadata import string_to_authors, \
authors_to_string
from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page
@ -110,10 +110,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
au = string_to_authors(au)
self.db.set_authors(id, au, notify=False)
if self.auto_author_sort.isChecked():
aut = self.db.authors(id, index_is_id=True)
aut = aut if aut else ''
aut = [a.strip().replace('|', ',') for a in aut.strip().split(',')]
x = authors_to_sort_string(aut)
x = self.db.author_sort_from_book(id, index_is_id=True)
if x:
self.db.set_author_sort(id, x, notify=False)
aus = unicode(self.author_sort.text())

View File

@ -23,7 +23,7 @@ from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, \
from calibre.ebooks.metadata import string_to_authors, \
authors_to_string, check_isbn
from calibre.ebooks.metadata.library_thing import cover_from_isbn
from calibre import islinux, isfreebsd
@ -460,7 +460,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
au = unicode(self.authors.text())
au = re.sub(r'\s+et al\.$', '', au)
authors = string_to_authors(au)
self.author_sort.setText(authors_to_sort_string(authors))
self.author_sort.setText(self.db.author_sort_from_authors(authors))
def swap_title_author(self):
title = self.title.text()

View File

@ -121,6 +121,9 @@
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>

View File

@ -420,8 +420,11 @@ class BooksModel(QAbstractTableModel): # {{{
pt.orig_file_path = os.path.abspath(src.name)
pt.seek(0)
if set_metadata:
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
try:
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
format)
except:
traceback.print_exc()
pt.close()
def to_uni(x):
if isbytestring(x):

View File

@ -13,15 +13,76 @@ from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
QPushButton, QWidget
QPushButton, QWidget, QItemDelegate, QString, QPen, \
QColor, QLinearGradient, QBrush
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.utils.config import prefs, tweaks
from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
class TagDelegate(QItemDelegate):
def __init__(self, parent):
QItemDelegate.__init__(self, parent)
self._parent = parent
self.icon = QIcon(I('star.png'))
def paint(self, painter, option, index):
item = index.internalPointer()
if item.type != TagTreeItem.TAG:
QItemDelegate.paint(self, painter, option, index)
return
r = option.rect
# Paint the decoration icon
icon = self._parent.model().data(index, Qt.DecorationRole).toPyObject()
icon.paint(painter, r, Qt.AlignLeft)
# Paint the rating, if any. The decoration icon is assumed to be square,
# filling the row top to bottom. The three is arbitrary, there to
# provide a little space between the icon and what follows
r.setLeft(r.left()+r.height()+3)
rating = item.tag.avg_rating
if config['show_avg_rating'] and item.tag.avg_rating is not None:
painter.save()
if tweaks['render_avg_rating_using'] == 'star':
painter.setClipRect(r.left(), r.top(),
int(r.height()*(rating/5.0)), r.height())
self.icon.paint(painter, r, Qt.AlignLeft | Qt.AlignVCenter)
r.setLeft(r.left() + r.height())
else:
painter.translate(r.left(), r.top())
# Compute factor so sizes can be expressed in percentages of the
# box defined by the row height
factor = r.height()/100.
width = 20
height = 80
left_offset = 5
top_offset = 10
if r > 0.0:
color = QColor(100, 100, 255) #medium blue, less glare
pen = QPen(color, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
painter.setPen(pen)
painter.scale(factor, factor)
painter.drawRect(left_offset, top_offset, width, height)
fill_height = height*(rating/5.0)
gradient = QLinearGradient(0, 0, 0, 100)
gradient.setColorAt(0.0, color)
gradient.setColorAt(1.0, color)
painter.setBrush(QBrush(gradient))
painter.drawRect(left_offset, top_offset+(height-fill_height),
width, fill_height)
# The '3' is arbitrary, there because we need a little space
# between the rectangle and the text.
r.setLeft(r.left() + ((width+left_offset*2)*factor) + 3)
painter.restore()
# Paint the text
painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter,
QString('[%d] %s'%(item.tag.count, item.tag.name)))
class TagsView(QTreeView): # {{{
@ -30,6 +91,7 @@ class TagsView(QTreeView): # {{{
user_category_edit = pyqtSignal(object)
tag_list_edit = pyqtSignal(object, object)
saved_search_edit = pyqtSignal(object)
author_sort_edit = pyqtSignal(object, object)
tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal()
@ -43,6 +105,7 @@ class TagsView(QTreeView): # {{{
self.setAlternatingRowColors(True)
self.setAnimated(True)
self.setHeaderHidden(True)
self.setItemDelegate(TagDelegate(self))
def set_database(self, db, tag_match, popularity):
self.hidden_categories = config['tag_browser_hidden_categories']
@ -112,6 +175,9 @@ class TagsView(QTreeView): # {{{
if action == 'manage_searches':
self.saved_search_edit.emit(category)
return
if action == 'edit_author_sort':
self.author_sort_edit.emit(self, index)
return
if action == 'hide':
self.hidden_categories.add(category)
elif action == 'show':
@ -132,6 +198,7 @@ class TagsView(QTreeView): # {{{
if item.type == TagTreeItem.TAG:
tag_item = item
tag_name = item.tag.name
tag_id = item.tag.id
item = item.parent
if item.type == TagTreeItem.CATEGORY:
category = unicode(item.name.toString())
@ -147,9 +214,13 @@ class TagsView(QTreeView): # {{{
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
self.db.field_metadata[key]['is_custom'] and \
self.db.field_metadata[key]['datatype'] != 'rating'):
self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
self.context_menu.addAction(_('Rename \'%s\'')%tag_name,
partial(self.context_menu_handler, action='edit_item',
category=tag_item, index=index))
if key == 'authors':
self.context_menu.addAction(_('Edit sort for \'%s\'')%tag_name,
partial(self.context_menu_handler,
action='edit_author_sort', index=tag_id))
self.context_menu.addSeparator()
# Hide/Show/Restore categories
self.context_menu.addAction(_('Hide category %s') % category,
@ -166,9 +237,12 @@ class TagsView(QTreeView): # {{{
self.context_menu.addSeparator()
if key in ['tags', 'publisher', 'series'] or \
self.db.field_metadata[key]['is_custom']:
self.context_menu.addAction(_('Manage ') + category,
self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='open_editor',
category=tag_name, key=key))
elif key == 'authors':
self.context_menu.addAction(_('Manage %s')%category,
partial(self.context_menu_handler, action='edit_author_sort'))
elif key == 'search':
self.context_menu.addAction(_('Manage Saved Searches'),
partial(self.context_menu_handler, action='manage_searches',
@ -298,7 +372,11 @@ class TagTreeItem(object): # {{{
if self.tag.count == 0:
return QVariant('%s'%(self.tag.name))
else:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
if self.tag.avg_rating is None:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
else:
return QVariant('[%d][%3.1f] %s'%(self.tag.count,
self.tag.avg_rating, self.tag.name))
if role == Qt.EditRole:
return QVariant(self.tag.name)
if role == Qt.DecorationRole:
@ -332,6 +410,7 @@ class TagsModel(QAbstractItemModel): # {{{
':custom' : QIcon(I('column.svg')),
':user' : QIcon(I('drawer.svg')),
'search' : QIcon(I('search.svg'))})
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db
@ -354,7 +433,14 @@ class TagsModel(QAbstractItemModel): # {{{
data=self.categories[i],
category_icon=self.category_icon_map[r],
tooltip=tt, category_key=r)
# This duplicates code in refresh(). Having it here as well
# can save seconds during startup, because we avoid a second
# call to get_node_tree.
for tag in data[r]:
if r not in self.categories_with_ratings and \
not self.db.field_metadata[r]['is_custom'] and \
not self.db.field_metadata[r]['kind'] == 'user':
tag.avg_rating = None
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
def set_search_restriction(self, s):
@ -417,6 +503,10 @@ class TagsModel(QAbstractItemModel): # {{{
if len(data[r]) > 0:
self.beginInsertRows(category_index, 0, len(data[r])-1)
for tag in data[r]:
if r not in self.categories_with_ratings and \
not self.db.field_metadata[r]['is_custom'] and \
not self.db.field_metadata[r]['kind'] == 'user':
tag.avg_rating = None
tag.state = state_map.get(tag.name, 0)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
self.endInsertRows()
@ -607,6 +697,7 @@ class TagBrowserMixin(object): # {{{
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.author_sort_edit.connect(self.do_author_sort_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.edit_categories.clicked.connect(lambda x:
@ -636,6 +727,19 @@ class TagBrowserMixin(object): # {{{
self.saved_search.clear_to_help()
self.search.clear_to_help()
def do_author_sort_edit(self, parent, id):
db = self.library_view.model().db
editor = EditAuthorsDialog(parent, db, id)
d = editor.exec_()
if d:
for (id, old_author, new_author, new_sort) in editor.result:
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))
self.library_view.model().refresh()
self.tags_view.recount()
# }}}
class TagBrowserWidget(QWidget): # {{{

View File

@ -461,14 +461,27 @@ class CustomColumns(object):
CREATE VIEW tag_browser_{table} AS SELECT
id,
value,
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link as bl,
ratings as r
WHERE {lt}.value={table}.id and bl.book={lt}.book and
r.id = bl.rating and r.rating <> 0) avg_rating
FROM {table};
CREATE VIEW tag_browser_filtered_{table} AS SELECT
id,
value,
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
books_list_filter(book)) count
books_list_filter(book)) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link as bl,
ratings as r
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
r.id = bl.rating AND r.rating <> 0 AND
books_list_filter(bl.book)) avg_rating
FROM {table};
'''.format(lt=lt, table=table),
@ -505,7 +518,6 @@ class CustomColumns(object):
END;
'''.format(table=table),
]
script = ' \n'.join(lines)
self.conn.executescript(script)
self.conn.commit()

View File

@ -12,7 +12,7 @@ from math import floor
from PyQt4.QtGui import QImage
from calibre.ebooks.metadata import title_sort
from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.library.database import LibraryDatabase
from calibre.library.field_metadata import FieldMetadata, TagsIcons
from calibre.library.schema_upgrades import SchemaUpgrade
@ -20,7 +20,7 @@ from calibre.library.caches import ResultCache
from calibre.library.custom_columns import CustomColumns
from calibre.library.sqlite import connect, IntegrityError, DBThread
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
MetaInformation, authors_to_sort_string
MetaInformation
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile
@ -56,11 +56,14 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
class Tag(object):
def __init__(self, name, id=None, count=0, state=0, tooltip=None, icon=None):
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
tooltip=None, icon=None):
self.name = name
self.id = id
self.count = count
self.state = state
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
self.tooltip = tooltip
self.icon = icon
@ -133,7 +136,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
id,
name,
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count,
(0) as avg_rating,
name as sort
FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
@ -144,7 +149,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_news AS SELECT DISTINCT
id,
name,
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count,
(0) as avg_rating,
name as sort
FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
@ -422,6 +429,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
mi.authors_sort_strings = self.authors_sort_strings(idx, index_is_id)
mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
@ -698,13 +706,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
continue
cn = cat['column']
if ids is None:
query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn, tn)
query = '''SELECT id, {0}, count, avg_rating, sort
FROM tag_browser_{1}'''.format(cn, tn)
else:
query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn, tn)
query = '''SELECT id, {0}, count, avg_rating, sort
FROM tag_browser_filtered_{1}'''.format(cn, tn)
if sort_on_count:
query += ' ORDER BY count DESC'
else:
query += ' ORDER BY {0} ASC'.format(cn)
query += ' ORDER BY sort ASC'
data = self.conn.get(query)
# icon_map is not None if get_categories is to store an icon and
@ -722,6 +732,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
datatype = cat['datatype']
if datatype == 'rating':
# eliminate the zero ratings line as well as count == 0
item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
formatter = (lambda x:u'\u2605'*int(round(x/2.)))
elif category == 'authors':
@ -733,15 +744,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
formatter = (lambda x:unicode(x))
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
icon=icon, tooltip = tooltip)
avg=r[3], sort=r[4],
icon=icon, tooltip=tooltip)
for r in data if item_not_zero_func(r)]
if category == 'series' and not sort_on_count:
if tweaks['title_series_sorting'] == 'library_order':
ts = lambda x: title_sort(x)
else:
ts = lambda x:x
categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(),
ts(y.name).lower()))
# We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically
@ -909,6 +914,38 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.set_path(id, True)
self.notify('metadata', [id])
# Given a book, return the list of author sort strings for the book's authors
def authors_sort_strings(self, id, index_is_id=False):
id = id if index_is_id else self.id(id)
aut_strings = self.conn.get('''
SELECT sort
FROM authors, books_authors_link as bl
WHERE bl.book=? and authors.id=bl.author
ORDER BY bl.id''', (id,))
result = []
for (sort,) in aut_strings:
result.append(sort)
return result
# Given a book, return the author_sort string for authors of the book
def author_sort_from_book(self, id, index_is_id=False):
auts = self.authors_sort_strings(id, index_is_id)
return ' & '.join(auts).replace('|', ',')
# Given a list of authors, return the author_sort string for the authors,
# preferring the author sort associated with the author over the computed
# string
def author_sort_from_authors(self, authors):
result = []
for aut in authors:
r = self.conn.get('SELECT sort FROM authors WHERE name=?',
(aut.replace(',', '|'),), all=False)
if r is None:
result.append(author_to_author_sort(aut))
else:
result.append(r)
return ' & '.join(result).replace('|', ',')
def set_authors(self, id, authors, notify=True):
'''
`authors`: A list of authors.
@ -935,7 +972,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
(id, aid))
except IntegrityError: # Sometimes books specify the same author twice in their metadata
pass
ss = authors_to_sort_string(authors)
self.conn.commit()
ss = self.author_sort_from_book(id, index_is_id=True)
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
(ss, id))
self.conn.commit()
@ -1007,6 +1045,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
def rename_tag(self, old_id, new_name):
new_name = new_name.strip()
new_id = self.conn.get(
'''SELECT id from tags
WHERE name=?''', (new_name,), all=False)
@ -1046,6 +1085,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
def rename_series(self, old_id, new_name):
new_name = new_name.strip()
new_id = self.conn.get(
'''SELECT id from series
WHERE name=?''', (new_name,), all=False)
@ -1075,7 +1115,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
index = index + 1
self.conn.commit()
def delete_series_using_id(self, 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,))
@ -1091,6 +1130,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result
def rename_publisher(self, old_id, new_name):
new_name = new_name.strip()
new_id = self.conn.get(
'''SELECT id from publishers
WHERE name=?''', (new_name,), all=False)
@ -1113,12 +1153,25 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_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 get_authors_with_ids(self):
result = self.conn.get('SELECT id,name,sort FROM authors')
if not result:
return []
return result
def set_sort_field_for_author(self, old_id, new_sort):
self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
(new_sort.strip(), old_id))
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)
def rename_author(self, old_id, new_name):
# Make sure that any commas in new_name are changed to '|'!
new_name = new_name.replace(',', '|')
new_name = new_name.replace(',', '|').strip()
# Get the list of books we must fix up, one way or the other
# Save the list so we can use it twice
@ -1141,7 +1194,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.execute('UPDATE authors SET name=? WHERE id=?',
(new_name, old_id))
self.conn.commit()
return
return new_id
# Author exists. To fix this, we must replace all the authors
# instead of replacing the one. Reason: db integrity checks can stop
# the rename process, which would leave everything half-done. We
@ -1184,24 +1237,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# 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
ORDER BY bl.id
''' , (book_id,))
# unpack the double-list structure
for i,aut in enumerate(authors):
authors[i] = aut[0]
ss = authors_to_sort_string(authors)
# Change the '|'s to ','
ss = ss.replace('|', ',')
self.conn.execute('''UPDATE books
SET author_sort=?
WHERE id=?''', (ss, book_id))
self.conn.commit()
ss = self.author_sort_from_book(book_id, index_is_id=True)
self.set_author_sort(book_id, ss)
# the caller will do a general refresh, so we don't need to
# do one here
return new_id
# end convenience methods
@ -1436,7 +1476,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if not add_duplicates and self.has_book(mi):
return None
series_index = 1.0 if mi.series_index is None else mi.series_index
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
title = mi.title
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
@ -1476,7 +1516,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
duplicates.append((path, format, mi))
continue
series_index = 1.0 if mi.series_index is None else mi.series_index
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
title = mi.title
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
@ -1515,7 +1555,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.title = _('Unknown')
if not mi.authors:
mi.authors = [_('Unknown')]
aus = mi.author_sort if mi.author_sort else authors_to_sort_string(mi.authors)
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace')
title = mi.title if isinstance(mi.title, unicode) else \

View File

@ -44,9 +44,12 @@ class FieldMetadata(dict):
is_category: is a tag browser category. If true, then:
table: name of the db table used to construct item list
column: name of the column in the normalized table to join on
link_column: name of the column in the connection table to join on
link_column: name of the column in the connection table to join on. This
key should not be present if there is no link table
category_sort: the field in the normalized table to sort on. This
key must be present if is_category is True
If these are None, then the category constructor must know how
to build the item list (e.g., formats).
to build the item list (e.g., formats, news).
The order below is the order that the categories will
appear in the tags pane.
@ -66,6 +69,7 @@ class FieldMetadata(dict):
('authors', {'table':'authors',
'column':'name',
'link_column':'author',
'category_sort':'sort',
'datatype':'text',
'is_multiple':',',
'kind':'field',
@ -76,6 +80,7 @@ class FieldMetadata(dict):
('series', {'table':'series',
'column':'name',
'link_column':'series',
'category_sort':'(title_sort(name))',
'datatype':'text',
'is_multiple':None,
'kind':'field',
@ -95,6 +100,7 @@ class FieldMetadata(dict):
('publisher', {'table':'publishers',
'column':'name',
'link_column':'publisher',
'category_sort':'name',
'datatype':'text',
'is_multiple':None,
'kind':'field',
@ -105,6 +111,7 @@ class FieldMetadata(dict):
('rating', {'table':'ratings',
'column':'rating',
'link_column':'rating',
'category_sort':'rating',
'datatype':'rating',
'is_multiple':None,
'kind':'field',
@ -114,6 +121,7 @@ class FieldMetadata(dict):
'is_category':True}),
('news', {'table':'news',
'column':'name',
'category_sort':'name',
'datatype':None,
'is_multiple':None,
'kind':'category',
@ -124,6 +132,7 @@ class FieldMetadata(dict):
('tags', {'table':'tags',
'column':'name',
'link_column': 'tag',
'category_sort':'name',
'datatype':'text',
'is_multiple':',',
'kind':'field',
@ -374,7 +383,7 @@ class FieldMetadata(dict):
'search_terms':[key], 'label':label,
'colnum':colnum, 'display':display,
'is_custom':True, 'is_category':is_category,
'link_column':'value',
'link_column':'value','category_sort':'value',
'is_editable': is_editable,}
self._add_search_terms_to_map(key, [key])
self.custom_label_to_key_map[label] = key

View File

@ -296,3 +296,117 @@ class SchemaUpgrade(object):
('books_%s_link'%field['table'],), all=False)
if table is not None:
create_tag_browser_view(field['table'], field['link_column'], field['column'])
def upgrade_version_11(self):
'Add average rating to tag browser views'
def create_std_tag_browser_view(table_name, column_name,
view_column_name, sort_column_name):
script = ('''
DROP VIEW IF EXISTS tag_browser_{tn};
CREATE VIEW tag_browser_{tn} AS SELECT
id,
{vcn},
(SELECT COUNT(id) FROM books_{tn}_link WHERE {cn}={tn}.id) count,
(SELECT AVG(ratings.rating)
FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings
WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND
ratings.id = bl.rating AND ratings.rating <> 0) avg_rating,
{scn} AS sort
FROM {tn};
DROP VIEW IF EXISTS tag_browser_filtered_{tn};
CREATE VIEW tag_browser_filtered_{tn} AS SELECT
id,
{vcn},
(SELECT COUNT(books_{tn}_link.id) FROM books_{tn}_link WHERE
{cn}={tn}.id AND books_list_filter(book)) count,
(SELECT AVG(ratings.rating)
FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings
WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND
ratings.id = bl.rating AND ratings.rating <> 0 AND
books_list_filter(bl.book)) avg_rating,
{scn} AS sort
FROM {tn};
'''.format(tn=table_name, cn=column_name,
vcn=view_column_name, scn= sort_column_name))
self.conn.executescript(script)
def create_cust_tag_browser_view(table_name, link_table_name):
script = '''
DROP VIEW IF EXISTS tag_browser_{table};
CREATE VIEW tag_browser_{table} AS SELECT
id,
value,
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link AS bl,
ratings AS r
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
r.id = bl.rating AND r.rating <> 0) avg_rating,
value AS sort
FROM {table};
DROP VIEW IF EXISTS tag_browser_filtered_{table};
CREATE VIEW tag_browser_filtered_{table} AS SELECT
id,
value,
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
books_list_filter(book)) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link AS bl,
ratings AS r
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
r.id = bl.rating AND r.rating <> 0 AND
books_list_filter(bl.book)) avg_rating,
value AS sort
FROM {table};
'''.format(lt=link_table_name, table=table_name)
self.conn.executescript(script)
for field in self.field_metadata.itervalues():
if field['is_category'] and not field['is_custom'] and 'link_column' in field:
table = self.conn.get(
'SELECT name FROM sqlite_master WHERE type="table" AND name=?',
('books_%s_link'%field['table'],), all=False)
if table is not None:
create_std_tag_browser_view(field['table'], field['link_column'],
field['column'], field['category_sort'])
db_tables = self.conn.get('''SELECT name FROM sqlite_master
WHERE type='table'
ORDER BY name''');
tables = []
for (table,) in db_tables:
tables.append(table)
for table in tables:
link_table = 'books_%s_link'%table
if table.startswith('custom_column_') and link_table in tables:
create_cust_tag_browser_view(table, link_table)
from calibre.ebooks.metadata import author_to_author_sort
aut = self.conn.get('SELECT id, name FROM authors');
records = []
for (id, author) in aut:
records.append((id, author.replace('|', ',')))
for id,author in records:
self.conn.execute('UPDATE authors SET sort=? WHERE id=?',
(author_to_author_sort(author.replace('|', ',')).strip(), id))
self.conn.commit()
self.conn.executescript('''
DROP TRIGGER IF EXISTS author_insert_trg;
CREATE TRIGGER author_insert_trg
AFTER INSERT ON authors
BEGIN
UPDATE authors SET sort=author_to_author_sort(NEW.name) WHERE id=NEW.id;
END;
DROP TRIGGER IF EXISTS author_update_trg;
CREATE TRIGGER author_update_trg
BEFORE UPDATE ON authors
BEGIN
UPDATE authors SET sort=author_to_author_sort(NEW.name)
WHERE id=NEW.id AND name <> NEW.name;
END;
''')

View File

@ -14,7 +14,7 @@ from Queue import Queue
from threading import RLock
from datetime import datetime
from calibre.ebooks.metadata import title_sort
from calibre.ebooks.metadata import title_sort, author_to_author_sort
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, isoformat
@ -116,10 +116,12 @@ class DBThread(Thread):
self.conn.create_aggregate('concat', 1, Concatenate)
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
if tweaks['title_series_sorting'] == 'library_order':
self.conn.create_function('title_sort', 1, title_sort)
else:
if tweaks['title_series_sorting'] == 'strictly_alphabetic':
self.conn.create_function('title_sort', 1, lambda x:x)
else:
self.conn.create_function('title_sort', 1, title_sort)
self.conn.create_function('author_to_author_sort', 1,
lambda x: author_to_author_sort(x.replace('|', ',')))
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
# Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1)