From de36d8acc9decd9937c29dd0ce4c91bda430aa67 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 11 Jun 2010 18:06:57 +0100 Subject: [PATCH 01/13] Prototype implementation of average ratings --- src/calibre/gui2/tag_view.py | 17 +++++- src/calibre/library/custom_columns.py | 19 +++++-- src/calibre/library/database2.py | 17 +++--- src/calibre/library/schema_upgrades.py | 73 ++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 11 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index bc698a3502..8b1a376bb5 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -298,7 +298,10 @@ 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 is None: + return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) + else: + return QVariant('[%d][%d] %s'%(self.tag.count, self.tag.avg, self.tag.name)) if role == Qt.EditRole: return QVariant(self.tag.name) if role == Qt.DecorationRole: @@ -332,6 +335,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 +358,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 = None TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) def set_search_restriction(self, s): @@ -417,6 +428,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 = None tag.state = state_map.get(tag.name, 0) t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map) self.endInsertRows() diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 23b78f38ae..91d04a4639 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -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,7 @@ class CustomColumns(object): END; '''.format(table=table), ] - + print lines script = ' \n'.join(lines) self.conn.executescript(script) self.conn.commit() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 7b98dc4537..41ad235d01 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -56,11 +56,12 @@ 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, tooltip=None, icon=None): self.name = name self.id = id self.count = count self.state = state + self.avg = avg/2 if avg is not None else 0 self.tooltip = tooltip self.icon = icon @@ -125,15 +126,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.connect() self.is_case_sensitive = not iswindows and not isosx and \ not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) - SchemaUpgrade.__init__(self) self.initialize_dynamic() + SchemaUpgrade.__init__(self) def initialize_dynamic(self): self.conn.executescript(u''' 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 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 +146,8 @@ 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 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 @@ -698,9 +701,9 @@ 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 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 FROM tag_browser_filtered_{1}'.format(cn, tn) if sort_on_count: query += ' ORDER BY count DESC' else: @@ -733,7 +736,7 @@ 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], 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': diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index 070ad1f3a6..0660a8b136 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -292,3 +292,76 @@ class SchemaUpgrade(object): for field in self.field_metadata.itervalues(): if field['is_category'] and not field['is_custom'] and 'link_column' in field: 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): + 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 + 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 + FROM {tn}; + + '''.format(tn=table_name, cn=column_name, vcn=view_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 + 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 + 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: + create_std_tag_browser_view(field['table'], field['link_column'], + field['column']) + + for field in self.field_metadata.itervalues(): + if field['is_category'] and field['is_custom']: + link_table_name = 'books_custom_column_%d_link'%field['colnum'] + print 'try to upgrade cust col', field['table'], link_table_name + create_cust_tag_browser_view(field['table'], link_table_name) From 79202dc8336eaf4fabe618d969ba5bab3b22803a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 11 Jun 2010 18:32:58 +0100 Subject: [PATCH 02/13] Get rid of print statement --- src/calibre/library/custom_columns.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 91d04a4639..c0ba91e252 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -518,7 +518,6 @@ class CustomColumns(object): END; '''.format(table=table), ] - print lines script = ' \n'.join(lines) self.conn.executescript(script) self.conn.commit() From 368eced25562ed0fddf9108cc3b284ca8a4c9742 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 11 Jun 2010 18:38:40 +0100 Subject: [PATCH 03/13] Make the average stay a floating point number --- src/calibre/gui2/tag_view.py | 2 +- src/calibre/library/database2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 8b1a376bb5..f1bbbe1c31 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -301,7 +301,7 @@ class TagTreeItem(object): # {{{ if self.tag.avg is None: return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) else: - return QVariant('[%d][%d] %s'%(self.tag.count, self.tag.avg, self.tag.name)) + return QVariant('[%d][%3.1f] %s'%(self.tag.count, self.tag.avg, self.tag.name)) if role == Qt.EditRole: return QVariant(self.tag.name) if role == Qt.DecorationRole: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index d014b250ba..04acca913c 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -61,7 +61,7 @@ class Tag(object): self.id = id self.count = count self.state = state - self.avg = avg/2 if avg is not None else 0 + self.avg = avg/2.0 if avg is not None else 0 self.tooltip = tooltip self.icon = icon From 97111f70a09be921f3dc65525962da23cb4e64f6 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 12 Jun 2010 16:58:51 +0100 Subject: [PATCH 04/13] Add graphical representation of rating --- src/calibre/gui2/tag_view.py | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index f1bbbe1c31..6ada763f80 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -9,11 +9,14 @@ Browsing book collection by tags. from itertools import izip from functools import partial +from math import cos, sin, pi from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \ QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, \ QPushButton, QWidget +from PyQt4.Qt import QItemDelegate, QString, QPainterPath, QPen, QColor, \ + QLinearGradient, QBrush from calibre.gui2 import config, NONE from calibre.utils.config import prefs @@ -23,6 +26,90 @@ from calibre.gui2 import error_dialog from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_list_editor import TagListEditor +class TagDelegate(QItemDelegate): + + def __init__(self, parent): + QItemDelegate.__init__(self, parent) + self._parent = parent + + def paint(self, painter, option, index): + + def draw_rating(rect, rating): + COLOR = QColor("blue") + if rating is None: + return 0 + painter.save() + painter.translate(r.left(), r.top()) + factor = r.height()/100. +# Try the star +# star_path = QPainterPath() +# star_path.moveTo(90, 50) +# for i in range(1, 5): +# star_path.lineTo(50 + 40 * cos(0.8 * i * pi), \ +# 50 + 40 * sin(0.8 * i * pi)) +# star_path.closeSubpath() +# star_path.setFillRule(Qt.WindingFill) +# pen = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) +# gradient = QLinearGradient(0, 0, 0, 100) +# gradient.setColorAt(0.0, COLOR) +# gradient.setColorAt(1.0, COLOR) +# painter.setBrush(QBrush(gradient)) +# painter.setClipRect(0, 0, int(r.height() * (rating/5.0)), r.height()) +# painter.scale(factor, factor) +# painter.translate(50.0, 50.0) +# painter.rotate(-20) +# painter.translate(-50.0, -50.0) +# painter.drawPath(star_path) +# painter.restore() +# return r.height() + +# Try a circle +# gradient = QLinearGradient(0, 0, 0, 100) +# gradient.setColorAt(0.0, COLOR) +# gradient.setColorAt(1.0, COLOR) +# painter.setBrush(QBrush(gradient)) +# painter.setClipRect(0, 0, int(r.height() * (rating/5.0)), r.height()) +# painter.scale(factor, factor) +# painter.drawEllipse(0, 0, 100, 100) +# painter.restore() +# return r.height() + +# Try a rectangle + width = 20 + height = 80 + left_offset = 5 + top_offset = 10 + if rating > 0.0: + 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) + painter.restore() + return int ((width+left_offset*2) * factor) + + item = index.internalPointer() + if item.type == TagTreeItem.TAG: + 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 + r.setLeft(r.left()+r.height()+5) + text_start = draw_rating(r, item.tag.avg) + # Paint the text + r.setLeft(r.left() + text_start+5) + painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, + QString('[%d] %s'%(item.tag.count, item.tag.name))) + else: + QItemDelegate.paint(self, painter, option, index) + class TagsView(QTreeView): # {{{ refresh_required = pyqtSignal() @@ -43,6 +130,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'] From 8ff2f2f865e86ae93265a1efc2351861370f17cd Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 13 Jun 2010 07:46:31 +0100 Subject: [PATCH 05/13] Finish average rating display. 1) change schema upgrade to not use field_metadata. 2) add option to preferences to show (or not) the average rating 3) add a tweak to choose between the clipped star and the rectangle 4) fix bug -- missing set of the search_as_you_type checkbox --- resources/default_tweaks.py | 7 + src/calibre/gui2/__init__.py | 2 + src/calibre/gui2/dialogs/config/__init__.py | 3 + src/calibre/gui2/dialogs/config/config.ui | 22 +++- src/calibre/gui2/tag_view.py | 138 ++++++++------------ src/calibre/library/database2.py | 4 +- src/calibre/library/schema_upgrades.py | 29 ++-- 7 files changed, 106 insertions(+), 99 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index bda839b28f..2075391da4 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -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' diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 3063ef252d..e4d98c1e4b 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -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='narrow') + 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() diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index b9c57b27ab..fa7bd8c2c5 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -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)) @@ -854,6 +856,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()) diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui index 917333a989..781b53f941 100644 --- a/src/calibre/gui2/dialogs/config/config.ui +++ b/src/calibre/gui2/dialogs/config/config.ui @@ -373,28 +373,38 @@ - Search as you type + Search as &you type true - + + + + Show &average ratings in the tags browser + + + true + + + + Automatically send downloaded &news to ebook reader - + &Delete news from library when it is automatically sent to reader - + @@ -411,7 +421,7 @@ - + Toolbar @@ -459,7 +469,7 @@ - + diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 6ada763f80..9919ef97a2 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -9,17 +9,15 @@ Browsing book collection by tags. from itertools import izip from functools import partial -from math import cos, sin, pi from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \ QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, \ QPushButton, QWidget -from PyQt4.Qt import QItemDelegate, QString, QPainterPath, QPen, QColor, \ - QLinearGradient, QBrush +from PyQt4.Qt import 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 @@ -31,84 +29,59 @@ 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): - - def draw_rating(rect, rating): - COLOR = QColor("blue") - if rating is None: - return 0 - painter.save() - painter.translate(r.left(), r.top()) - factor = r.height()/100. -# Try the star -# star_path = QPainterPath() -# star_path.moveTo(90, 50) -# for i in range(1, 5): -# star_path.lineTo(50 + 40 * cos(0.8 * i * pi), \ -# 50 + 40 * sin(0.8 * i * pi)) -# star_path.closeSubpath() -# star_path.setFillRule(Qt.WindingFill) -# pen = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) -# gradient = QLinearGradient(0, 0, 0, 100) -# gradient.setColorAt(0.0, COLOR) -# gradient.setColorAt(1.0, COLOR) -# painter.setBrush(QBrush(gradient)) -# painter.setClipRect(0, 0, int(r.height() * (rating/5.0)), r.height()) -# painter.scale(factor, factor) -# painter.translate(50.0, 50.0) -# painter.rotate(-20) -# painter.translate(-50.0, -50.0) -# painter.drawPath(star_path) -# painter.restore() -# return r.height() - -# Try a circle -# gradient = QLinearGradient(0, 0, 0, 100) -# gradient.setColorAt(0.0, COLOR) -# gradient.setColorAt(1.0, COLOR) -# painter.setBrush(QBrush(gradient)) -# painter.setClipRect(0, 0, int(r.height() * (rating/5.0)), r.height()) -# painter.scale(factor, factor) -# painter.drawEllipse(0, 0, 100, 100) -# painter.restore() -# return r.height() - -# Try a rectangle - width = 20 - height = 80 - left_offset = 5 - top_offset = 10 - if rating > 0.0: - 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) - painter.restore() - return int ((width+left_offset*2) * factor) - item = index.internalPointer() - if item.type == TagTreeItem.TAG: - 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 - r.setLeft(r.left()+r.height()+5) - text_start = draw_rating(r, item.tag.avg) - # Paint the text - r.setLeft(r.left() + text_start+5) - painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, - QString('[%d] %s'%(item.tag.count, item.tag.name))) - else: + 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): # {{{ @@ -386,10 +359,11 @@ class TagTreeItem(object): # {{{ if self.tag.count == 0: return QVariant('%s'%(self.tag.name)) else: - if self.tag.avg is None: + 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, self.tag.name)) + 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: @@ -453,7 +427,7 @@ class TagsModel(QAbstractItemModel): # {{{ 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 = None + tag.avg_rating = None TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) def set_search_restriction(self, s): @@ -519,7 +493,7 @@ class TagsModel(QAbstractItemModel): # {{{ 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 = None + 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() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 04acca913c..939ab92f38 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -61,7 +61,7 @@ class Tag(object): self.id = id self.count = count self.state = state - self.avg = avg/2.0 if avg is not None else 0 + self.avg_rating = avg/2.0 if avg is not None else 0 self.tooltip = tooltip self.icon = icon @@ -126,8 +126,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.connect() self.is_case_sensitive = not iswindows and not isosx and \ not os.path.exists(self.dbpath.replace('metadata.db', 'MeTAdAtA.dB')) - self.initialize_dynamic() SchemaUpgrade.__init__(self) + self.initialize_dynamic() def initialize_dynamic(self): self.conn.executescript(u''' diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index 0660a8b136..870254f999 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -355,13 +355,24 @@ class SchemaUpgrade(object): '''.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: - create_std_tag_browser_view(field['table'], field['link_column'], - field['column']) + STANDARD_TAG_BROWSER_TABLES = [ + ('authors', 'author', 'name'), + ('publishers', 'publisher', 'name'), + ('ratings', 'rating', 'rating'), + ('series', 'series', 'name'), + ('tags', 'tag', 'name'), + ] + for table, column, view_column in STANDARD_TAG_BROWSER_TABLES: + create_std_tag_browser_view(table, column, view_column) - for field in self.field_metadata.itervalues(): - if field['is_category'] and field['is_custom']: - link_table_name = 'books_custom_column_%d_link'%field['colnum'] - print 'try to upgrade cust col', field['table'], link_table_name - create_cust_tag_browser_view(field['table'], link_table_name) + 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: + print table + create_cust_tag_browser_view(table, link_table) \ No newline at end of file From 669dd8024c6eb1f4e5b2961747565c393b140743 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 13 Jun 2010 22:58:46 +0100 Subject: [PATCH 06/13] Author_sort in author table changes --- src/calibre/gui2/convert/metadata.py | 4 +- src/calibre/gui2/device.py | 4 +- src/calibre/gui2/dialogs/metadata_bulk.py | 7 +- src/calibre/gui2/dialogs/metadata_single.py | 4 +- src/calibre/gui2/dialogs/sort_field_dialog.py | 16 ++++ src/calibre/gui2/dialogs/sort_field_dialog.ui | 83 +++++++++++++++++ src/calibre/gui2/tag_view.py | 21 +++++ src/calibre/library/database2.py | 93 +++++++++++++------ src/calibre/library/field_metadata.py | 3 +- src/calibre/library/schema_upgrades.py | 61 ++++++++---- src/calibre/library/sqlite.py | 4 +- 11 files changed, 242 insertions(+), 58 deletions(-) create mode 100644 src/calibre/gui2/dialogs/sort_field_dialog.py create mode 100644 src/calibre/gui2/dialogs/sort_field_dialog.ui diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index 2026f1cee5..3ddd5674bb 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -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): diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index cf54e6c1f3..c8eb4c2403 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -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: diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index eca7fe9c15..8b27ff1999 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -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()) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 0241e1b542..1543df458e 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -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 @@ -459,7 +459,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() diff --git a/src/calibre/gui2/dialogs/sort_field_dialog.py b/src/calibre/gui2/dialogs/sort_field_dialog.py new file mode 100644 index 0000000000..d1c6d45ed3 --- /dev/null +++ b/src/calibre/gui2/dialogs/sort_field_dialog.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' +__license__ = 'GPL v3' + +from PyQt4.Qt import QDialog +from calibre.gui2.dialogs.sort_field_dialog_ui import Ui_SortFieldDialog + +class SortFieldDialog(QDialog, Ui_SortFieldDialog): + + def __init__(self, parent, text): + QDialog.__init__(self, parent) + Ui_SortFieldDialog.__init__(self) + self.setupUi(self) + if text is not None: + self.textbox.setText(text) diff --git a/src/calibre/gui2/dialogs/sort_field_dialog.ui b/src/calibre/gui2/dialogs/sort_field_dialog.ui new file mode 100644 index 0000000000..3fc386d1ef --- /dev/null +++ b/src/calibre/gui2/dialogs/sort_field_dialog.ui @@ -0,0 +1,83 @@ + + + SortFieldDialog + + + + 0 + 0 + 334 + 135 + + + + + 0 + 0 + + + + Edit sort field + + + + + 10 + 10 + 311 + 111 + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + buttonBox + accepted() + SortFieldDialog + accept() + + + 229 + 211 + + + 157 + 234 + + + + + buttonBox + rejected() + SortFieldDialog + reject() + + + 297 + 217 + + + 286 + 234 + + + + + diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 9919ef97a2..bae81c79cd 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -23,6 +23,7 @@ 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.sort_field_dialog import SortFieldDialog class TagDelegate(QItemDelegate): @@ -90,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, object) tag_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal() @@ -173,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, category, index) + return if action == 'hide': self.hidden_categories.add(category) elif action == 'show': @@ -193,6 +198,8 @@ class TagsView(QTreeView): # {{{ if item.type == TagTreeItem.TAG: tag_item = item tag_name = item.tag.name + tag_id = item.tag.id + tag_sort = item.tag.sort item = item.parent if item.type == TagTreeItem.CATEGORY: category = unicode(item.name.toString()) @@ -211,6 +218,10 @@ class TagsView(QTreeView): # {{{ self.context_menu.addAction(_('Rename') + " '" + 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') + " '" + tag_name + "'", + partial(self.context_menu_handler, action='edit_author_sort', + category=tag_sort, index=tag_id)) self.context_menu.addSeparator() # Hide/Show/Restore categories self.context_menu.addAction(_('Hide category %s') % category, @@ -684,6 +695,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: @@ -713,6 +725,15 @@ class TagBrowserMixin(object): # {{{ self.saved_search.clear_to_help() self.search.clear_to_help() + def do_author_sort_edit(self, parent, text, id): + editor = SortFieldDialog(parent, text) + d = editor.exec_() + if d: + print editor.textbox.text() + self.library_view.model().db.set_sort_field_for_author \ + (id, unicode(editor.textbox.text())) + self.tags_view.recount() + # }}} class TagBrowserWidget(QWidget): # {{{ diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 939ab92f38..144b66b5e4 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -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,12 +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, avg=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 @@ -135,7 +137,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): id, name, (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count, - (0) as avg_rating + (0) as avg_rating, + (null) 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 @@ -147,7 +150,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): id, name, (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count, - (0) as avg_rating + (0) as avg_rating, + (null) 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 @@ -425,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) @@ -701,12 +706,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): continue cn = cat['column'] if ids is None: - query = 'SELECT id, {0}, count, avg_rating 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, avg_rating FROM tag_browser_filtered_{1}'.format(cn, tn) + query = '''SELECT id, {0}, count, avg_rating + FROM tag_browser_filtered_{1}'''.format(cn, tn) if sort_on_count: query += ' ORDER BY count DESC' else: + if 'category_sort' in cat: + cn = cat['category_sort'] query += ' ORDER BY {0} ASC'.format(cn) data = self.conn.get(query) @@ -736,7 +745,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): formatter = (lambda x:unicode(x)) categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], - avg=r[3], 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': @@ -912,6 +922,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: + aut = aut.replace(',', '|') + r = self.conn.get('SELECT sort FROM authors WHERE name=?', (aut,), 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. @@ -938,7 +980,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() @@ -1117,7 +1160,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.commit() # There is no editor for author, so we do not need get_authors_with_ids or - # delete_author_using_id. + # delete_author_using_id. However, we can change the author's sort field, so + # we provide that setter + + def set_sort_field_for_author(self, old_id, new_sort): + self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \ + (new_sort, old_id)) + self.conn.commit() def rename_author(self, old_id, new_name): # Make sure that any commas in new_name are changed to '|'! @@ -1187,22 +1236,8 @@ 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 @@ -1439,7 +1474,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') @@ -1479,7 +1514,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') @@ -1518,7 +1553,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 \ diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 82e4edfdf2..535893b24c 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -72,7 +72,8 @@ class FieldMetadata(dict): 'name':_('Authors'), 'search_terms':['authors', 'author'], 'is_custom':False, - 'is_category':True}), + 'is_category':True, + 'category_sort':'sort'}), ('series', {'table':'series', 'column':'name', 'link_column':'series', diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index 870254f999..5763cbad70 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -295,7 +295,8 @@ class SchemaUpgrade(object): 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): + 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 @@ -305,22 +306,25 @@ class SchemaUpgrade(object): (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 + 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}, + {vcn} as sort, (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 + books_list_filter(bl.book)) avg_rating, + {scn} as sort FROM {tn}; - '''.format(tn=table_name, cn=column_name, vcn=view_column_name)) + '''.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): @@ -335,7 +339,8 @@ class SchemaUpgrade(object): 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 + r.id = bl.rating and r.rating <> 0) avg_rating, + value as sort FROM {table}; DROP VIEW IF EXISTS tag_browser_filtered_{table}; @@ -350,20 +355,21 @@ class SchemaUpgrade(object): 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 + books_list_filter(bl.book)) avg_rating, + value as sort FROM {table}; '''.format(lt=link_table_name, table=table_name) self.conn.executescript(script) STANDARD_TAG_BROWSER_TABLES = [ - ('authors', 'author', 'name'), - ('publishers', 'publisher', 'name'), - ('ratings', 'rating', 'rating'), - ('series', 'series', 'name'), - ('tags', 'tag', 'name'), + ('authors', 'author', 'name', 'sort'), + ('publishers', 'publisher', 'name', 'name'), + ('ratings', 'rating', 'rating', 'rating'), + ('series', 'series', 'name', 'name'), + ('tags', 'tag', 'name', 'name'), ] - for table, column, view_column in STANDARD_TAG_BROWSER_TABLES: - create_std_tag_browser_view(table, column, view_column) + for table, column, view_column, sort_column in STANDARD_TAG_BROWSER_TABLES: + create_std_tag_browser_view(table, column, view_column, sort_column) db_tables = self.conn.get('''SELECT name FROM sqlite_master WHERE type='table' @@ -374,5 +380,28 @@ class SchemaUpgrade(object): for table in tables: link_table = 'books_%s_link'%table if table.startswith('custom_column_') and link_table in tables: - print table - create_cust_tag_browser_view(table, link_table) \ No newline at end of file + 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(''' + 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; + 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; + ''') \ No newline at end of file diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index adf6691671..9aab71ab79 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -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 @@ -120,6 +120,8 @@ class DBThread(Thread): self.conn.create_function('title_sort', 1, title_sort) else: self.conn.create_function('title_sort', 1, lambda x:x) + 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) From 34312daed3cb04544a48bdc0483495c95ec1752f Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Jun 2010 10:30:13 +0100 Subject: [PATCH 07/13] Improvements to the new tag browser views. --- src/calibre/library/database2.py | 16 ++++------------ src/calibre/library/field_metadata.py | 18 +++++++++++++----- src/calibre/library/schema_upgrades.py | 14 +++++--------- src/calibre/library/sqlite.py | 6 +++--- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 144b66b5e4..a4e8b4ff77 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -138,7 +138,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): name, (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count, (0) as avg_rating, - (null) as sort + 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 @@ -151,7 +151,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): name, (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count, (0) as avg_rating, - (null) as sort + 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 @@ -714,9 +714,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if sort_on_count: query += ' ORDER BY count DESC' else: - if 'category_sort' in cat: - cn = cat['category_sort'] - 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 @@ -734,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': @@ -748,13 +747,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 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 diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 535893b24c..8cb5c9bdad 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -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,17 +69,18 @@ class FieldMetadata(dict): ('authors', {'table':'authors', 'column':'name', 'link_column':'author', + 'category_sort':'sort', 'datatype':'text', 'is_multiple':',', 'kind':'field', 'name':_('Authors'), 'search_terms':['authors', 'author'], 'is_custom':False, - 'is_category':True, - 'category_sort':'sort'}), + 'is_category':True}), ('series', {'table':'series', 'column':'name', 'link_column':'series', + 'category_sort':'(title_sort(name))', 'datatype':'text', 'is_multiple':None, 'kind':'field', @@ -96,6 +100,7 @@ class FieldMetadata(dict): ('publisher', {'table':'publishers', 'column':'name', 'link_column':'publisher', + 'category_sort':'name', 'datatype':'text', 'is_multiple':None, 'kind':'field', @@ -106,6 +111,7 @@ class FieldMetadata(dict): ('rating', {'table':'ratings', 'column':'rating', 'link_column':'rating', + 'category_sort':'rating', 'datatype':'rating', 'is_multiple':None, 'kind':'field', @@ -115,6 +121,7 @@ class FieldMetadata(dict): 'is_category':True}), ('news', {'table':'news', 'column':'name', + 'category_sort':'name', 'datatype':None, 'is_multiple':None, 'kind':'category', @@ -125,6 +132,7 @@ class FieldMetadata(dict): ('tags', {'table':'tags', 'column':'name', 'link_column': 'tag', + 'category_sort':'name', 'datatype':'text', 'is_multiple':',', 'kind':'field', @@ -375,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 diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index 5763cbad70..fd3762cc94 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -361,15 +361,11 @@ class SchemaUpgrade(object): '''.format(lt=link_table_name, table=table_name) self.conn.executescript(script) - STANDARD_TAG_BROWSER_TABLES = [ - ('authors', 'author', 'name', 'sort'), - ('publishers', 'publisher', 'name', 'name'), - ('ratings', 'rating', 'rating', 'rating'), - ('series', 'series', 'name', 'name'), - ('tags', 'tag', 'name', 'name'), - ] - for table, column, view_column, sort_column in STANDARD_TAG_BROWSER_TABLES: - create_std_tag_browser_view(table, column, view_column, sort_column) + for field in self.field_metadata.itervalues(): + if field['is_category'] and not field['is_custom'] and 'link_column' in field: + print field['table'] + 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' diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index 9aab71ab79..7e0458fba4 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -116,10 +116,10 @@ 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())) From 5dfbc21472e5555b4c4aa3292b2720f67f595b96 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Jun 2010 10:40:22 +0100 Subject: [PATCH 08/13] Fix up conflicts in preferences ordering on the interface screen --- src/calibre/gui2/dialogs/config/config.ui | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui index b66e16a487..ba92c0d301 100644 --- a/src/calibre/gui2/dialogs/config/config.ui +++ b/src/calibre/gui2/dialogs/config/config.ui @@ -370,16 +370,6 @@ - - - - Search as you type - - - true - - - @@ -390,21 +380,31 @@ - + + + + Search as you type + + + true + + + + Automatically send downloaded &news to ebook reader - + &Delete news from library when it is automatically sent to reader - + @@ -421,7 +421,7 @@ - + Toolbar @@ -469,7 +469,7 @@ - + From db507d4b13c53cfa334068ddf744f015d1cfbca2 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Jun 2010 12:29:32 +0100 Subject: [PATCH 09/13] First iteration at an authors management dialog --- src/calibre/gui2/dialogs/sort_field_dialog.py | 62 +++++++++++++++++-- src/calibre/gui2/dialogs/sort_field_dialog.ui | 14 +++-- src/calibre/gui2/tag_view.py | 11 ++-- src/calibre/library/database2.py | 9 +-- 4 files changed, 79 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/dialogs/sort_field_dialog.py b/src/calibre/gui2/dialogs/sort_field_dialog.py index d1c6d45ed3..a73d0d8cf4 100644 --- a/src/calibre/gui2/dialogs/sort_field_dialog.py +++ b/src/calibre/gui2/dialogs/sort_field_dialog.py @@ -3,14 +3,68 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __license__ = 'GPL v3' -from PyQt4.Qt import QDialog +from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView + +from calibre.ebooks.metadata import author_to_author_sort from calibre.gui2.dialogs.sort_field_dialog_ui import Ui_SortFieldDialog +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 SortFieldDialog(QDialog, Ui_SortFieldDialog): - def __init__(self, parent, text): + def __init__(self, parent, db, id_to_select): QDialog.__init__(self, parent) Ui_SortFieldDialog.__init__(self) self.setupUi(self) - if text is not None: - self.textbox.setText(text) + + self.buttonBox.accepted.connect(self.accepted) + self.table.cellChanged.connect(self.cell_changed) + + 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 = aut + + if select_item is not None: + self.table.setCurrentItem(select_item) + self.table.resizeColumnsToContents() + self.table.setSortingEnabled(True) + self.table.sortByColumn(1, Qt.AscendingOrder) + + def accepted(self): + print 'accepted!' + 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()) + sort = unicode(self.table.item(row, 1).text()) + print id, aut, sort + 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: + aut = unicode(self.table.item(row, 0).text()) + c = self.table.item(row, 1) + if c is not None: + c.setText(author_to_author_sort(aut)) diff --git a/src/calibre/gui2/dialogs/sort_field_dialog.ui b/src/calibre/gui2/dialogs/sort_field_dialog.ui index 3fc386d1ef..bc6ffae4fc 100644 --- a/src/calibre/gui2/dialogs/sort_field_dialog.ui +++ b/src/calibre/gui2/dialogs/sort_field_dialog.ui @@ -6,8 +6,8 @@ 0 0 - 334 - 135 + 518 + 262 @@ -24,13 +24,17 @@ 10 10 - 311 - 111 + 501 + 231 - + + + 0 + + diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index bae81c79cd..27d73a0af8 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -726,12 +726,15 @@ class TagBrowserMixin(object): # {{{ self.search.clear_to_help() def do_author_sort_edit(self, parent, text, id): - editor = SortFieldDialog(parent, text) + editor = SortFieldDialog(parent, self.library_view.model().db, id) d = editor.exec_() if d: - print editor.textbox.text() - self.library_view.model().db.set_sort_field_for_author \ - (id, unicode(editor.textbox.text())) + print editor.result + for (id, old_author, new_author, new_sort) in editor.result: + if old_author != new_author: + self.library_view.model().db.rename_author(id, new_author) + self.library_view.model().db.set_sort_field_for_author \ + (id, unicode(new_sort)) self.tags_view.recount() # }}} diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index a4e8b4ff77..794474f821 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1113,7 +1113,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,)) @@ -1151,9 +1150,11 @@ 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. However, we can change the author's sort field, so - # we provide that setter + 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=?', \ From f4854022a0db4d19f185462e9e665f46b4371e4b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Jun 2010 13:43:05 +0100 Subject: [PATCH 10/13] Edit authors works. --- ...field_dialog.py => edit_authors_dialog.py} | 12 +-- .../gui2/dialogs/edit_authors_dialog.ui | 89 +++++++++++++++++++ src/calibre/gui2/dialogs/sort_field_dialog.ui | 87 ------------------ src/calibre/gui2/dialogs/tag_list_editor.ui | 3 + src/calibre/gui2/tag_view.py | 33 +++---- src/calibre/library/database2.py | 8 +- 6 files changed, 123 insertions(+), 109 deletions(-) rename src/calibre/gui2/dialogs/{sort_field_dialog.py => edit_authors_dialog.py} (88%) create mode 100644 src/calibre/gui2/dialogs/edit_authors_dialog.ui delete mode 100644 src/calibre/gui2/dialogs/sort_field_dialog.ui diff --git a/src/calibre/gui2/dialogs/sort_field_dialog.py b/src/calibre/gui2/dialogs/edit_authors_dialog.py similarity index 88% rename from src/calibre/gui2/dialogs/sort_field_dialog.py rename to src/calibre/gui2/dialogs/edit_authors_dialog.py index a73d0d8cf4..8d13f98f7a 100644 --- a/src/calibre/gui2/dialogs/sort_field_dialog.py +++ b/src/calibre/gui2/dialogs/edit_authors_dialog.py @@ -6,7 +6,7 @@ __license__ = 'GPL v3' from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView from calibre.ebooks.metadata import author_to_author_sort -from calibre.gui2.dialogs.sort_field_dialog_ui import Ui_SortFieldDialog +from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog class tableItem(QTableWidgetItem): def __ge__(self, other): @@ -15,11 +15,11 @@ class tableItem(QTableWidgetItem): def __lt__(self, other): return unicode(self.text()).lower() < unicode(other.text()).lower() -class SortFieldDialog(QDialog, Ui_SortFieldDialog): +class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): def __init__(self, parent, db, id_to_select): QDialog.__init__(self, parent) - Ui_SortFieldDialog.__init__(self) + Ui_EditAuthorsDialog.__init__(self) self.setupUi(self) self.buttonBox.accepted.connect(self.accepted) @@ -42,22 +42,21 @@ class SortFieldDialog(QDialog, Ui_SortFieldDialog): self.table.setItem(row, 0, aut) self.table.setItem(row, 1, sort) if id == id_to_select: - select_item = aut + select_item = sort if select_item is not None: self.table.setCurrentItem(select_item) + self.table.editItem(select_item) self.table.resizeColumnsToContents() self.table.setSortingEnabled(True) self.table.sortByColumn(1, Qt.AscendingOrder) def accepted(self): - print 'accepted!' 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()) sort = unicode(self.table.item(row, 1).text()) - print id, aut, sort orig_aut,orig_sort = self.authors[id] if orig_aut != aut or orig_sort != sort: self.result.append((id, orig_aut, aut, sort)) @@ -68,3 +67,4 @@ class SortFieldDialog(QDialog, Ui_SortFieldDialog): c = self.table.item(row, 1) if c is not None: c.setText(author_to_author_sort(aut)) + self.table.setCurrentItem(c) diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.ui b/src/calibre/gui2/dialogs/edit_authors_dialog.ui new file mode 100644 index 0000000000..4ac133700f --- /dev/null +++ b/src/calibre/gui2/dialogs/edit_authors_dialog.ui @@ -0,0 +1,89 @@ + + + EditAuthorsDialog + + + + 0 + 0 + 410 + 239 + + + + + 0 + 0 + + + + Manage authors + + + + + + + 0 + 0 + + + + 0 + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + buttonBox + table + buttonBox + + + + + buttonBox + accepted() + EditAuthorsDialog + accept() + + + 229 + 211 + + + 157 + 234 + + + + + buttonBox + rejected() + EditAuthorsDialog + reject() + + + 297 + 217 + + + 286 + 234 + + + + + diff --git a/src/calibre/gui2/dialogs/sort_field_dialog.ui b/src/calibre/gui2/dialogs/sort_field_dialog.ui deleted file mode 100644 index bc6ffae4fc..0000000000 --- a/src/calibre/gui2/dialogs/sort_field_dialog.ui +++ /dev/null @@ -1,87 +0,0 @@ - - - SortFieldDialog - - - - 0 - 0 - 518 - 262 - - - - - 0 - 0 - - - - Edit sort field - - - - - 10 - 10 - 501 - 231 - - - - - - - 0 - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - buttonBox - accepted() - SortFieldDialog - accept() - - - 229 - 211 - - - 157 - 234 - - - - - buttonBox - rejected() - SortFieldDialog - reject() - - - 297 - 217 - - - 286 - 234 - - - - - diff --git a/src/calibre/gui2/dialogs/tag_list_editor.ui b/src/calibre/gui2/dialogs/tag_list_editor.ui index 4f57af745b..39076aa1f6 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.ui +++ b/src/calibre/gui2/dialogs/tag_list_editor.ui @@ -121,6 +121,9 @@ QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + true + diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 27d73a0af8..83463128bd 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -23,7 +23,7 @@ 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.sort_field_dialog import SortFieldDialog +from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog class TagDelegate(QItemDelegate): @@ -91,7 +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, object) + author_sort_edit = pyqtSignal(object, object) tag_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal() @@ -176,7 +176,7 @@ class TagsView(QTreeView): # {{{ self.saved_search_edit.emit(category) return if action == 'edit_author_sort': - self.author_sort_edit.emit(self, category, index) + self.author_sort_edit.emit(self, index) return if action == 'hide': self.hidden_categories.add(category) @@ -199,7 +199,6 @@ class TagsView(QTreeView): # {{{ tag_item = item tag_name = item.tag.name tag_id = item.tag.id - tag_sort = item.tag.sort item = item.parent if item.type == TagTreeItem.CATEGORY: category = unicode(item.name.toString()) @@ -215,13 +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') + " '" + tag_name + "'", - partial(self.context_menu_handler, action='edit_author_sort', - category=tag_sort, index=tag_id)) + 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, @@ -238,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', @@ -725,16 +727,17 @@ class TagBrowserMixin(object): # {{{ self.saved_search.clear_to_help() self.search.clear_to_help() - def do_author_sort_edit(self, parent, text, id): - editor = SortFieldDialog(parent, self.library_view.model().db, id) + def do_author_sort_edit(self, parent, id): + db = self.library_view.model().db + editor = EditAuthorsDialog(parent, db, id) d = editor.exec_() if d: - print editor.result for (id, old_author, new_author, new_sort) in editor.result: if old_author != new_author: - self.library_view.model().db.rename_author(id, new_author) - self.library_view.model().db.set_sort_field_for_author \ - (id, unicode(new_sort)) + # 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() # }}} diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 794474f821..e2302c1c77 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1160,6 +1160,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \ (new_sort, 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 '|'! @@ -1186,7 +1191,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 @@ -1233,6 +1238,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 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 From 36f6e670221b05b0df7cb2c9c7d59f9739677c10 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Jun 2010 14:56:15 +0100 Subject: [PATCH 11/13] Small cleanups after testing --- .../gui2/dialogs/edit_authors_dialog.py | 28 +++++++++++++------ src/calibre/library/database2.py | 4 +-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.py b/src/calibre/gui2/dialogs/edit_authors_dialog.py index 8d13f98f7a..6e7eef3add 100644 --- a/src/calibre/gui2/dialogs/edit_authors_dialog.py +++ b/src/calibre/gui2/dialogs/edit_authors_dialog.py @@ -23,7 +23,6 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): self.setupUi(self) self.buttonBox.accepted.connect(self.accepted) - self.table.cellChanged.connect(self.cell_changed) self.table.setSelectionMode(QAbstractItemView.SingleSelection) self.table.setColumnCount(2) @@ -43,13 +42,18 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): 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) - self.table.resizeColumnsToContents() - self.table.setSortingEnabled(True) - self.table.sortByColumn(1, Qt.AscendingOrder) + else: + self.table.setCurrentCell(0, 0) def accepted(self): self.result = [] @@ -63,8 +67,16 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): def cell_changed(self, row, col): if col == 0: - aut = unicode(self.table.item(row, 0).text()) + item = self.table.item(row, 0) + aut = unicode(item.text()) c = self.table.item(row, 1) - if c is not None: - c.setText(author_to_author_sort(aut)) - self.table.setCurrentItem(c) + 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) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index e2302c1c77..72f33b677c 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -938,8 +938,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def author_sort_from_authors(self, authors): result = [] for aut in authors: - aut = aut.replace(',', '|') - r = self.conn.get('SELECT sort FROM authors WHERE name=?', (aut,), all=False) + 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: From 4ed3c284636b7a3c9d4d2b84ba196f8584242226 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Jun 2010 16:36:30 +0100 Subject: [PATCH 12/13] Fix restrictions --- src/calibre/library/database2.py | 2 +- src/calibre/library/schema_upgrades.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 72f33b677c..a8ac6ce5bf 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -709,7 +709,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): query = '''SELECT id, {0}, count, avg_rating, sort FROM tag_browser_{1}'''.format(cn, tn) else: - query = '''SELECT id, {0}, count, avg_rating + 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' diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index 0d7df2f0cd..fcf27c4183 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -316,7 +316,7 @@ class SchemaUpgrade(object): DROP VIEW IF EXISTS tag_browser_filtered_{tn}; CREATE VIEW tag_browser_filtered_{tn} AS SELECT id, - {vcn} as sort, + {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 34ff4216d5b4700c50125b8e902c27183f225ab0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 14 Jun 2010 16:58:14 +0100 Subject: [PATCH 13/13] Ensure that strings are stripped before they are used --- src/calibre/gui2/dialogs/edit_authors_dialog.py | 6 +++--- src/calibre/library/database2.py | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.py b/src/calibre/gui2/dialogs/edit_authors_dialog.py index 6e7eef3add..842fd7c943 100644 --- a/src/calibre/gui2/dialogs/edit_authors_dialog.py +++ b/src/calibre/gui2/dialogs/edit_authors_dialog.py @@ -59,8 +59,8 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): 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()) - sort = unicode(self.table.item(row, 1).text()) + 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)) @@ -68,7 +68,7 @@ class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): def cell_changed(self, row, col): if col == 0: item = self.table.item(row, 0) - aut = unicode(item.text()) + aut = unicode(item.text()).strip() c = self.table.item(row, 1) c.setText(author_to_author_sort(aut)) item = c diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index a8ac6ce5bf..2fb22a27f4 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1045,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) @@ -1084,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) @@ -1128,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) @@ -1158,7 +1161,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): def set_sort_field_for_author(self, old_id, new_sort): self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \ - (new_sort, old_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,)) @@ -1168,7 +1171,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): 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