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 # order until the title is edited. Double-clicking on a title and hitting return
# without changing anything is sufficient to change the sort. # without changing anything is sufficient to change the sort.
title_series_sorting = 'library_order' 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): def author_to_author_sort(author):
method = tweaks['author_sort_copy_method'] 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 return author
tokens = author.split() tokens = author.split()
tokens = tokens[-1:] + tokens[:-1] tokens = tokens[-1:] + tokens[:-1]

View File

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

View File

@ -101,6 +101,8 @@ def _config():
help=_('tag browser categories not to display')) help=_('tag browser categories not to display'))
c.add_opt('gui_layout', choices=['wide', 'narrow'], c.add_opt('gui_layout', choices=['wide', 'narrow'],
help=_('The layout of the user interface'), default='wide') 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) return ConfigProxy(c)
config = _config() 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 import choose_images, error_dialog
from calibre.gui2.convert.metadata_ui import Ui_Form from calibre.gui2.convert.metadata_ui import Ui_Form
from calibre.ebooks.metadata import authors_to_string, string_to_authors, \ 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.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2.convert import Widget from calibre.gui2.convert import Widget
@ -57,7 +57,7 @@ class MetadataWidget(Widget, Ui_Form):
au = unicode(self.author.currentText()) au = unicode(self.author.currentText())
au = re.sub(r'\s+et al\.$', '', au) au = re.sub(r'\s+et al\.$', '', au)
authors = string_to_authors(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): 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, \ from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
pixmap_to_data, warning_dialog, \ pixmap_to_data, warning_dialog, \
question_dialog, info_dialog, choose_dir 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 import preferred_encoding, prints
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.devices.errors import FreeSpaceError from calibre.devices.errors import FreeSpaceError
@ -1409,7 +1409,7 @@ class DeviceMixin(object): # {{{
# Set author_sort if it isn't already # Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None) asort = getattr(book, 'author_sort', None)
if not asort and book.authors: 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 resend_metadata = True
if resend_metadata: 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.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
self.device_detection_button.clicked.connect(self.debug_device_detection) self.device_detection_button.clicked.connect(self.debug_device_detection)
self.port.editingFinished.connect(self.check_port_value) 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', self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
True)) True))
li = None li = None
@ -862,6 +864,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked() config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
config['upload_news_to_device'] = self.sync_news.isChecked() config['upload_news_to_device'] = self.sync_news.isChecked()
config['search_as_you_type'] = self.search_as_you_type.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['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_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()) config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())

View File

@ -371,6 +371,16 @@
</widget> </widget>
</item> </item>
<item row="5" column="0"> <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"> <widget class="QCheckBox" name="search_as_you_type">
<property name="text"> <property name="text">
<string>Search as you type</string> <string>Search as you type</string>
@ -380,21 +390,21 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0" colspan="2"> <item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="sync_news"> <widget class="QCheckBox" name="sync_news">
<property name="text"> <property name="text">
<string>Automatically send downloaded &amp;news to ebook reader</string> <string>Automatically send downloaded &amp;news to ebook reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="0" colspan="2"> <item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news"> <widget class="QCheckBox" name="delete_news">
<property name="text"> <property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string> <string>&amp;Delete news from library when it is automatically sent to reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="0" colspan="2"> <item row="9" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
@ -411,7 +421,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="9" column="0" colspan="2"> <item row="10" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2"> <widget class="QGroupBox" name="groupBox_2">
<property name="title"> <property name="title">
<string>Toolbar</string> <string>Toolbar</string>
@ -459,7 +469,7 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="10" column="0" colspan="2"> <item row="11" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_7"> <layout class="QHBoxLayout" name="horizontalLayout_7">
<item> <item>
<widget class="QGroupBox" name="groupBox"> <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.metadata_bulk_ui import Ui_MetadataBulkDialog
from calibre.gui2.dialogs.tag_editor import TagEditor 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 authors_to_string
from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page 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) au = string_to_authors(au)
self.db.set_authors(id, au, notify=False) self.db.set_authors(id, au, notify=False)
if self.auto_author_sort.isChecked(): if self.auto_author_sort.isChecked():
aut = self.db.authors(id, index_is_id=True) x = self.db.author_sort_from_book(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)
if x: if x:
self.db.set_author_sort(id, x, notify=False) self.db.set_author_sort(id, x, notify=False)
aus = unicode(self.author_sort.text()) 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.dialogs.tag_editor import TagEditor
from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS 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 authors_to_string, check_isbn
from calibre.ebooks.metadata.library_thing import cover_from_isbn from calibre.ebooks.metadata.library_thing import cover_from_isbn
from calibre import islinux, isfreebsd from calibre import islinux, isfreebsd
@ -460,7 +460,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
au = unicode(self.authors.text()) au = unicode(self.authors.text())
au = re.sub(r'\s+et al\.$', '', au) au = re.sub(r'\s+et al\.$', '', au)
authors = string_to_authors(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): def swap_title_author(self):
title = self.title.text() title = self.title.text()

View File

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

View File

@ -420,8 +420,11 @@ class BooksModel(QAbstractTableModel): # {{{
pt.orig_file_path = os.path.abspath(src.name) pt.orig_file_path = os.path.abspath(src.name)
pt.seek(0) pt.seek(0)
if set_metadata: 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) format)
except:
traceback.print_exc()
pt.close() pt.close()
def to_uni(x): def to_uni(x):
if isbytestring(x): if isbytestring(x):

View File

@ -13,15 +13,76 @@ from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
QAbstractItemModel, QVariant, QModelIndex, QMenu, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, \
QPushButton, QWidget QPushButton, QWidget, QItemDelegate, QString, QPen, \
QColor, QLinearGradient, QBrush
from calibre.gui2 import config, NONE 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.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor 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): # {{{ class TagsView(QTreeView): # {{{
@ -30,6 +91,7 @@ class TagsView(QTreeView): # {{{
user_category_edit = pyqtSignal(object) user_category_edit = pyqtSignal(object)
tag_list_edit = pyqtSignal(object, object) tag_list_edit = pyqtSignal(object, object)
saved_search_edit = pyqtSignal(object) saved_search_edit = pyqtSignal(object)
author_sort_edit = pyqtSignal(object, object)
tag_item_renamed = pyqtSignal() tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal()
@ -43,6 +105,7 @@ class TagsView(QTreeView): # {{{
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setAnimated(True) self.setAnimated(True)
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.setItemDelegate(TagDelegate(self))
def set_database(self, db, tag_match, popularity): def set_database(self, db, tag_match, popularity):
self.hidden_categories = config['tag_browser_hidden_categories'] self.hidden_categories = config['tag_browser_hidden_categories']
@ -112,6 +175,9 @@ class TagsView(QTreeView): # {{{
if action == 'manage_searches': if action == 'manage_searches':
self.saved_search_edit.emit(category) self.saved_search_edit.emit(category)
return return
if action == 'edit_author_sort':
self.author_sort_edit.emit(self, index)
return
if action == 'hide': if action == 'hide':
self.hidden_categories.add(category) self.hidden_categories.add(category)
elif action == 'show': elif action == 'show':
@ -132,6 +198,7 @@ class TagsView(QTreeView): # {{{
if item.type == TagTreeItem.TAG: if item.type == TagTreeItem.TAG:
tag_item = item tag_item = item
tag_name = item.tag.name tag_name = item.tag.name
tag_id = item.tag.id
item = item.parent item = item.parent
if item.type == TagTreeItem.CATEGORY: if item.type == TagTreeItem.CATEGORY:
category = unicode(item.name.toString()) category = unicode(item.name.toString())
@ -147,9 +214,13 @@ class TagsView(QTreeView): # {{{
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \ (key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
self.db.field_metadata[key]['is_custom'] and \ self.db.field_metadata[key]['is_custom'] and \
self.db.field_metadata[key]['datatype'] != 'rating'): 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', partial(self.context_menu_handler, action='edit_item',
category=tag_item, index=index)) 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() self.context_menu.addSeparator()
# Hide/Show/Restore categories # Hide/Show/Restore categories
self.context_menu.addAction(_('Hide category %s') % category, self.context_menu.addAction(_('Hide category %s') % category,
@ -166,9 +237,12 @@ class TagsView(QTreeView): # {{{
self.context_menu.addSeparator() self.context_menu.addSeparator()
if key in ['tags', 'publisher', 'series'] or \ if key in ['tags', 'publisher', 'series'] or \
self.db.field_metadata[key]['is_custom']: 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', partial(self.context_menu_handler, action='open_editor',
category=tag_name, key=key)) 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': elif key == 'search':
self.context_menu.addAction(_('Manage Saved Searches'), self.context_menu.addAction(_('Manage Saved Searches'),
partial(self.context_menu_handler, action='manage_searches', partial(self.context_menu_handler, action='manage_searches',
@ -298,7 +372,11 @@ class TagTreeItem(object): # {{{
if self.tag.count == 0: if self.tag.count == 0:
return QVariant('%s'%(self.tag.name)) return QVariant('%s'%(self.tag.name))
else: 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: if role == Qt.EditRole:
return QVariant(self.tag.name) return QVariant(self.tag.name)
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
@ -332,6 +410,7 @@ class TagsModel(QAbstractItemModel): # {{{
':custom' : QIcon(I('column.svg')), ':custom' : QIcon(I('column.svg')),
':user' : QIcon(I('drawer.svg')), ':user' : QIcon(I('drawer.svg')),
'search' : QIcon(I('search.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.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.db = db self.db = db
@ -354,7 +433,14 @@ class TagsModel(QAbstractItemModel): # {{{
data=self.categories[i], data=self.categories[i],
category_icon=self.category_icon_map[r], category_icon=self.category_icon_map[r],
tooltip=tt, category_key=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]: 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) TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
def set_search_restriction(self, s): def set_search_restriction(self, s):
@ -417,6 +503,10 @@ class TagsModel(QAbstractItemModel): # {{{
if len(data[r]) > 0: if len(data[r]) > 0:
self.beginInsertRows(category_index, 0, len(data[r])-1) self.beginInsertRows(category_index, 0, len(data[r])-1)
for tag in data[r]: 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) tag.state = state_map.get(tag.name, 0)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map) t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
self.endInsertRows() self.endInsertRows()
@ -607,6 +697,7 @@ class TagBrowserMixin(object): # {{{
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit) 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.user_category_edit.connect(self.do_user_categories_edit)
self.tags_view.saved_search_edit.connect(self.do_saved_search_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.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help) self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
self.edit_categories.clicked.connect(lambda x: self.edit_categories.clicked.connect(lambda x:
@ -636,6 +727,19 @@ class TagBrowserMixin(object): # {{{
self.saved_search.clear_to_help() self.saved_search.clear_to_help()
self.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): # {{{ class TagBrowserWidget(QWidget): # {{{

View File

@ -461,14 +461,27 @@ class CustomColumns(object):
CREATE VIEW tag_browser_{table} AS SELECT CREATE VIEW tag_browser_{table} AS SELECT
id, id,
value, 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}; FROM {table};
CREATE VIEW tag_browser_filtered_{table} AS SELECT CREATE VIEW tag_browser_filtered_{table} AS SELECT
id, id,
value, value,
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND (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}; FROM {table};
'''.format(lt=lt, table=table), '''.format(lt=lt, table=table),
@ -505,7 +518,6 @@ class CustomColumns(object):
END; END;
'''.format(table=table), '''.format(table=table),
] ]
script = ' \n'.join(lines) script = ' \n'.join(lines)
self.conn.executescript(script) self.conn.executescript(script)
self.conn.commit() self.conn.commit()

View File

@ -12,7 +12,7 @@ from math import floor
from PyQt4.QtGui import QImage 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.database import LibraryDatabase
from calibre.library.field_metadata import FieldMetadata, TagsIcons from calibre.library.field_metadata import FieldMetadata, TagsIcons
from calibre.library.schema_upgrades import SchemaUpgrade 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.custom_columns import CustomColumns
from calibre.library.sqlite import connect, IntegrityError, DBThread from calibre.library.sqlite import connect, IntegrityError, DBThread
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \ 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.ebooks.metadata.meta import get_metadata, metadata_from_formats
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
@ -56,11 +56,14 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
class Tag(object): 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.name = name
self.id = id self.id = id
self.count = count self.count = count
self.state = state self.state = state
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
self.tooltip = tooltip self.tooltip = tooltip
self.icon = icon self.icon = icon
@ -133,7 +136,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
id, id,
name, 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 FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN (SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag 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 CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_news AS SELECT DISTINCT
id, id,
name, 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 FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN (SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag 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(',')] if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum) 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.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.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(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) mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
@ -698,13 +706,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
continue continue
cn = cat['column'] cn = cat['column']
if ids is None: 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: 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: if sort_on_count:
query += ' ORDER BY count DESC' query += ' ORDER BY count DESC'
else: else:
query += ' ORDER BY {0} ASC'.format(cn) query += ' ORDER BY sort ASC'
data = self.conn.get(query) data = self.conn.get(query)
# icon_map is not None if get_categories is to store an icon and # 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'] datatype = cat['datatype']
if datatype == 'rating': 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) item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
formatter = (lambda x:u'\u2605'*int(round(x/2.))) formatter = (lambda x:u'\u2605'*int(round(x/2.)))
elif category == 'authors': elif category == 'authors':
@ -733,15 +744,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
formatter = (lambda x:unicode(x)) formatter = (lambda x:unicode(x))
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], 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)] 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 # We delayed computing the standard formats category because it does not
# use a view, but is computed dynamically # use a view, but is computed dynamically
@ -909,6 +914,38 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.set_path(id, True) self.set_path(id, True)
self.notify('metadata', [id]) 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): def set_authors(self, id, authors, notify=True):
''' '''
`authors`: A list of authors. `authors`: A list of authors.
@ -935,7 +972,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
(id, aid)) (id, aid))
except IntegrityError: # Sometimes books specify the same author twice in their metadata except IntegrityError: # Sometimes books specify the same author twice in their metadata
pass 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=?', self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
(ss, id)) (ss, id))
self.conn.commit() self.conn.commit()
@ -1007,6 +1045,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result return result
def rename_tag(self, old_id, new_name): def rename_tag(self, old_id, new_name):
new_name = new_name.strip()
new_id = self.conn.get( new_id = self.conn.get(
'''SELECT id from tags '''SELECT id from tags
WHERE name=?''', (new_name,), all=False) WHERE name=?''', (new_name,), all=False)
@ -1046,6 +1085,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result return result
def rename_series(self, old_id, new_name): def rename_series(self, old_id, new_name):
new_name = new_name.strip()
new_id = self.conn.get( new_id = self.conn.get(
'''SELECT id from series '''SELECT id from series
WHERE name=?''', (new_name,), all=False) WHERE name=?''', (new_name,), all=False)
@ -1075,7 +1115,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
index = index + 1 index = index + 1
self.conn.commit() self.conn.commit()
def delete_series_using_id(self, id): def delete_series_using_id(self, id):
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (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 books_series_link WHERE series=?', (id,))
@ -1091,6 +1130,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return result return result
def rename_publisher(self, old_id, new_name): def rename_publisher(self, old_id, new_name):
new_name = new_name.strip()
new_id = self.conn.get( new_id = self.conn.get(
'''SELECT id from publishers '''SELECT id from publishers
WHERE name=?''', (new_name,), all=False) 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.execute('DELETE FROM publishers WHERE id=?', (old_id,))
self.conn.commit() self.conn.commit()
# There is no editor for author, so we do not need get_authors_with_ids or def get_authors_with_ids(self):
# delete_author_using_id. 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): def rename_author(self, old_id, new_name):
# Make sure that any commas in new_name are changed to '|'! # 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 # Get the list of books we must fix up, one way or the other
# Save the list so we can use it twice # 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=?', self.conn.execute('UPDATE authors SET name=? WHERE id=?',
(new_name, old_id)) (new_name, old_id))
self.conn.commit() self.conn.commit()
return return new_id
# Author exists. To fix this, we must replace all the authors # Author exists. To fix this, we must replace all the authors
# instead of replacing the one. Reason: db integrity checks can stop # instead of replacing the one. Reason: db integrity checks can stop
# the rename process, which would leave everything half-done. We # the rename process, which would leave everything half-done. We
@ -1184,24 +1237,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# now fix the filesystem paths # now fix the filesystem paths
self.set_path(book_id, index_is_id=True) self.set_path(book_id, index_is_id=True)
# Next fix the author sort. Reset it to the default # Next fix the author sort. Reset it to the default
authors = self.conn.get(''' ss = self.author_sort_from_book(book_id, index_is_id=True)
SELECT authors.name self.set_author_sort(book_id, ss)
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()
# the caller will do a general refresh, so we don't need to # the caller will do a general refresh, so we don't need to
# do one here # do one here
return new_id
# end convenience methods # end convenience methods
@ -1436,7 +1476,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if not add_duplicates and self.has_book(mi): if not add_duplicates and self.has_book(mi):
return None return None
series_index = 1.0 if mi.series_index is None else mi.series_index 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 title = mi.title
if isinstance(aus, str): if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace') aus = aus.decode(preferred_encoding, 'replace')
@ -1476,7 +1516,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
duplicates.append((path, format, mi)) duplicates.append((path, format, mi))
continue continue
series_index = 1.0 if mi.series_index is None else mi.series_index 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 title = mi.title
if isinstance(aus, str): if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace') aus = aus.decode(preferred_encoding, 'replace')
@ -1515,7 +1555,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
mi.title = _('Unknown') mi.title = _('Unknown')
if not mi.authors: if not mi.authors:
mi.authors = [_('Unknown')] 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): if isinstance(aus, str):
aus = aus.decode(preferred_encoding, 'replace') aus = aus.decode(preferred_encoding, 'replace')
title = mi.title if isinstance(mi.title, unicode) else \ 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: is_category: is a tag browser category. If true, then:
table: name of the db table used to construct item list table: name of the db table used to construct item list
column: name of the column in the normalized table to join on 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 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 The order below is the order that the categories will
appear in the tags pane. appear in the tags pane.
@ -66,6 +69,7 @@ class FieldMetadata(dict):
('authors', {'table':'authors', ('authors', {'table':'authors',
'column':'name', 'column':'name',
'link_column':'author', 'link_column':'author',
'category_sort':'sort',
'datatype':'text', 'datatype':'text',
'is_multiple':',', 'is_multiple':',',
'kind':'field', 'kind':'field',
@ -76,6 +80,7 @@ class FieldMetadata(dict):
('series', {'table':'series', ('series', {'table':'series',
'column':'name', 'column':'name',
'link_column':'series', 'link_column':'series',
'category_sort':'(title_sort(name))',
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
@ -95,6 +100,7 @@ class FieldMetadata(dict):
('publisher', {'table':'publishers', ('publisher', {'table':'publishers',
'column':'name', 'column':'name',
'link_column':'publisher', 'link_column':'publisher',
'category_sort':'name',
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
@ -105,6 +111,7 @@ class FieldMetadata(dict):
('rating', {'table':'ratings', ('rating', {'table':'ratings',
'column':'rating', 'column':'rating',
'link_column':'rating', 'link_column':'rating',
'category_sort':'rating',
'datatype':'rating', 'datatype':'rating',
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
@ -114,6 +121,7 @@ class FieldMetadata(dict):
'is_category':True}), 'is_category':True}),
('news', {'table':'news', ('news', {'table':'news',
'column':'name', 'column':'name',
'category_sort':'name',
'datatype':None, 'datatype':None,
'is_multiple':None, 'is_multiple':None,
'kind':'category', 'kind':'category',
@ -124,6 +132,7 @@ class FieldMetadata(dict):
('tags', {'table':'tags', ('tags', {'table':'tags',
'column':'name', 'column':'name',
'link_column': 'tag', 'link_column': 'tag',
'category_sort':'name',
'datatype':'text', 'datatype':'text',
'is_multiple':',', 'is_multiple':',',
'kind':'field', 'kind':'field',
@ -374,7 +383,7 @@ class FieldMetadata(dict):
'search_terms':[key], 'label':label, 'search_terms':[key], 'label':label,
'colnum':colnum, 'display':display, 'colnum':colnum, 'display':display,
'is_custom':True, 'is_category':is_category, 'is_custom':True, 'is_category':is_category,
'link_column':'value', 'link_column':'value','category_sort':'value',
'is_editable': is_editable,} 'is_editable': is_editable,}
self._add_search_terms_to_map(key, [key]) self._add_search_terms_to_map(key, [key])
self.custom_label_to_key_map[label] = 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) ('books_%s_link'%field['table'],), all=False)
if table is not None: if table is not None:
create_tag_browser_view(field['table'], field['link_column'], field['column']) 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 threading import RLock
from datetime import datetime 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.config import tweaks
from calibre.utils.date import parse_date, isoformat 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('concat', 1, Concatenate)
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate) self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
if tweaks['title_series_sorting'] == 'library_order': if tweaks['title_series_sorting'] == 'strictly_alphabetic':
self.conn.create_function('title_sort', 1, title_sort)
else:
self.conn.create_function('title_sort', 1, lambda x:x) 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())) self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
# Dummy functions for dynamically created filters # Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1) self.conn.create_function('books_list_filter', 1, lambda x: 1)