From de36d8acc9decd9937c29dd0ce4c91bda430aa67 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 11 Jun 2010 18:06:57 +0100 Subject: [PATCH 01/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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/26] 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 From 6e9e4b1b5c66ea4d61c1f42562425cef84495a26 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 07:51:42 -0600 Subject: [PATCH 14/26] Fix regression in uploading book to device --- src/calibre/gui2/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index c8eb4c2403..b3a7196b20 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -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 = self.db.author_sort_from_authors(book.authors) + book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors) resend_metadata = True if resend_metadata: From 899a13defc2acff9c03d14215741f982e2c23d00 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 07:58:22 -0600 Subject: [PATCH 15/26] Content server: In OPDS feeds use the new author sort information when generating feeds by author --- src/calibre/library/server/opds.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index d396d73af2..7b8d609dda 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -99,17 +99,20 @@ def html_to_lxml(raw): raw = etree.tostring(root, encoding=None) return etree.fromstring(raw) -def CATALOG_ENTRY(item, base_href, version, updated): +def CATALOG_ENTRY(item, base_href, version, updated, ignore_count=False): id_ = 'calibre:category:'+item.name iid = 'N' + item.name if item.id is not None: iid = 'I' + str(item.id) link = NAVLINK(href = base_href + '/' + hexlify(iid)) + count = _('%d books')%item.count + if ignore_count: + count = '' return E.entry( TITLE(item.name), ID(id_), UPDATED(updated), - E.content(_('%d books')%item.count, type='text'), + E.content(count, type='text'), link ) @@ -265,8 +268,12 @@ class CategoryFeed(NavFeed): def __init__(self, items, which, id_, updated, version, offsets, page_url, up_url): NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url) base_href = self.base_href + '/category/' + hexlify(which) + ignore_count = False + if which == 'search': + ignore_count = True for item in items: - self.root.append(CATALOG_ENTRY(item, base_href, version, updated)) + self.root.append(CATALOG_ENTRY(item, base_href, version, updated, + ignore_count=ignore_count)) class CategoryGroupFeed(NavFeed): @@ -393,7 +400,7 @@ class OPDSServer(object): owhich = hexlify('N'+which) up_url = url_for('opdsnavcatalog', version, which=owhich) items = categories[category] - items = [x for x in items if x.name.startswith(which)] + items = [x for x in items if getattr(x, 'sort', x.name).startswith(which)] if not items: raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category, which)) @@ -458,11 +465,11 @@ class OPDSServer(object): def __init__(self, text, count): self.text, self.count = text, count - starts = set([x.name[0] for x in items]) + starts = set([getattr(x, 'sort', x.name)[0] for x in items]) category_groups = OrderedDict() for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())): category_groups[x] = len([y for y in items if - y.name.startswith(x)]) + getattr(y, 'sort', y.name).startswith(x)]) items = [Group(x, y) for x, y in category_groups.items()] max_items = self.opts.max_opds_items offsets = OPDSOffsets(offset, max_items, len(items)) From 64f0678559dd86c6ce6057d28ef3b6ebb12f4216 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 08:29:23 -0600 Subject: [PATCH 16/26] Better bookmarks icon --- resources/images/bookmarks.svg | 712 +++++++++++---------------------- 1 file changed, 239 insertions(+), 473 deletions(-) diff --git a/resources/images/bookmarks.svg b/resources/images/bookmarks.svg index 2fcd844283..6964853702 100644 --- a/resources/images/bookmarks.svg +++ b/resources/images/bookmarks.svg @@ -1,7 +1,6 @@ + sodipodi:docbase="/home/dobey/Projects/gnome-icon-theme/scalable/apps" + sodipodi:docname="accessories-dictionary.svg" + inkscape:export-filename="/home/ulisse/Desktop/accessories-dictionary.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + inkscape:output_extension="org.inkscape.output.svg.inkscape"> - - - - - - - - - - - - - - - - + id="linearGradient2309"> + id="stop2311" /> + id="stop2313" /> + id="linearGradient2301"> + id="stop2303" /> + id="stop2305" /> + inkscape:collect="always" + id="linearGradient2286"> + id="stop2288" /> - - + id="stop2290" /> + inkscape:collect="always" + id="linearGradient2276"> + id="stop2278" /> + id="stop2280" /> + inkscape:collect="always" + id="linearGradient2258"> + id="stop2260" /> + id="stop2262" /> + + + + + + + + + style="stop-color:#babdb6" /> + style="stop-color:#d3d7cf;stop-opacity:0;" /> + inkscape:collect="always" + id="linearGradient2184"> + id="stop2186" /> + id="stop2188" /> + gradientTransform="matrix(-1,0,0,1,48,0)" /> + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + inkscape:grid-points="true" + gridspacingx="0.5px" + gridspacingy="0.5px" + gridempspacing="2" + inkscape:window-width="872" + inkscape:window-height="694" + inkscape:window-x="0" + inkscape:window-y="25" + fill="#75507b" /> @@ -458,133 +249,108 @@ image/svg+xml + + + Ulisse Perusin + + + Dictionary + + + dictionary + translation + + + + + + + + + + + - - - + style="opacity:0.50196078;color:#000000;fill:url(#radialGradient2292);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible" + id="path2284" + sodipodi:cx="24" + sodipodi:cy="36.75" + sodipodi:rx="22.5" + sodipodi:ry="6.75" + d="M 46.5 36.75 A 22.5 6.75 0 1 1 1.5,36.75 A 22.5 6.75 0 1 1 46.5 36.75 z" + transform="matrix(1.066667,0,0,0.962963,-1.600001,1.111111)" /> + style="color:#000000;fill:#523856;fill-opacity:1;fill-rule:nonzero;stroke:#3e263b;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible" + d="M 4.5,11.5 L 43.5,11.5 L 47.5,38.5 L 29,38.5 L 28,37.5 C 26,39 22,39 20,37.5 L 19,38.5 L 0.5,38.5 L 4.5,11.5 z " + id="rect1304" + sodipodi:nodetypes="ccccccccc" /> + sodipodi:type="inkscape:offset" + inkscape:radius="-0.91809106" + inkscape:original="M 4.5 11.5 L 0.5 38.5 L 19 38.5 L 20 37.5 C 22 39 26 39 28 37.5 L 29 38.5 L 47.5 38.5 L 43.5 11.5 L 4.5 11.5 z " + xlink:href="#rect1304" + style="opacity:0.13333333;color:#000000;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible" + id="path2274" + inkscape:href="#rect1304" + d="M 5.28125,12.40625 L 1.5625,37.59375 L 18.59375,37.59375 L 19.34375,36.84375 C 19.667151,36.507336 20.191452,36.467006 20.5625,36.75 C 21.327469,37.323727 22.653015,37.71875 24,37.71875 C 25.346985,37.71875 26.672531,37.323727 27.4375,36.75 C 27.808548,36.467006 28.332849,36.507336 28.65625,36.84375 L 29.40625,37.59375 L 46.4375,37.59375 L 42.71875,12.40625 L 5.28125,12.40625 z " /> + style="fill:url(#linearGradient2282);fill-opacity:1.0;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 2,36.5 C 7.6666667,36.5 16,35 19,36.5 C 22,34 26,34 29,36.5 C 32,35 41,36.5 46,36.5 L 45.5,34 C 38.5,31.5 29,28.5 24,33 C 19,28.5 9.5,31.5 2.5,34 L 2,36.5 z " + id="path2180" + sodipodi:nodetypes="cccccccc" /> + sodipodi:type="inkscape:offset" + inkscape:radius="-1.0582203" + inkscape:original="M 14 30.875 C 10.125 31.375 6 32.75 2.5 34 L 2 36.5 C 7.6666667 36.5 16 35 19 36.5 C 22 34 26 34 29 36.5 C 32 35 41 36.5 46 36.5 L 45.5 34 C 38.5 31.5 29 28.5 24 33 C 21.5 30.75 17.875 30.375 14 30.875 z " + xlink:href="#path2180" + style="opacity:0.30196078;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2315);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path2266" + inkscape:href="#path2180" + d="M 14.375,31.9375 C 10.963293,32.392394 7.260823,33.622273 3.90625,34.8125 L 3.8125,35.34375 C 6.2979599,35.262594 9.0476285,35.037732 11.6875,34.875 C 14.462294,34.703951 16.881256,34.711661 18.78125,35.40625 C 20.133116,34.409774 21.661646,33.894157 23.21875,33.75 C 21.042747,31.830616 17.941674,31.461944 14.375,31.9375 z M 28.625,31.9375 C 27.145571,32.213473 25.86037,32.798142 24.78125,33.75 C 26.338354,33.894157 27.866884,34.409774 29.21875,35.40625 C 31.163554,34.697135 33.704549,34.703523 36.5625,34.875 C 39.261382,35.036933 41.920385,35.260963 44.1875,35.34375 L 44.09375,34.8125 C 40.739177,33.622273 37.036707,32.392394 33.625,31.9375 C 31.827105,31.697781 30.128781,31.656984 28.625,31.9375 z " /> + style="fill:url(#linearGradient2245);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2247);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 2.5,34 C 9,31.5 20,29 24,33 C 28,29 39,31.5 45.5,34 L 42.5,10.5 C 37,8 27.5,6 24,9 C 20,6 12,8 5.5,10.5 L 2.5,34 z " + id="path2182" + sodipodi:nodetypes="ccccccc" /> - - - - - - - + style="color:#000000;fill:url(#linearGradient2211);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible" + d="M 24,9.5 C 25.221264,8.803878 26.327771,7.9069322 28,8 L 29,30.5 C 27.5,30 25.5,31.5 24,32.5 L 24,9.5 z " + id="path2195" + sodipodi:nodetypes="ccccc" /> + + From 9455a93d78310e226c462fb0d1057038c353696c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 15 Jun 2010 16:03:43 +0100 Subject: [PATCH 17/26] Added: 1) The addition of tooltips to the Tag Browser displaying the average rating. 2) Also, change the sort by popularity to a combobox to allow sorting by average rating. --- src/calibre/gui2/__init__.py | 4 ++-- src/calibre/gui2/tag_view.py | 41 +++++++++++++++++++------------- src/calibre/library/database2.py | 34 ++++++++++++++++---------- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 306bbc77e6..1056f6ced6 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -43,8 +43,8 @@ def _config(): help=_('Notify when a new version is available')) c.add_opt('use_roman_numerals_for_series_number', default=True, help=_('Use Roman numerals for series number')) - c.add_opt('sort_by_popularity', default=False, - help=_('Sort tags list by popularity')) + c.add_opt('sort_tags_by', default='name', + help=_('Sort tags list by name, popularity, or rating')) c.add_opt('cover_flow_queue_length', default=6, help=_('Number of covers to show in the cover browsing mode')) c.add_opt('LRF_conversion_defaults', default=[], diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 9cc90ca83f..c3c5e8954d 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -81,8 +81,12 @@ class TagDelegate(QItemDelegate): 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))) + if item.tag.count == 0: + painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, + QString('%s'%(item.tag.name))) + else: + painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, + QString('[%d] %s'%(item.tag.count, item.tag.name))) class TagsView(QTreeView): # {{{ @@ -107,12 +111,12 @@ class TagsView(QTreeView): # {{{ self.setHeaderHidden(True) self.setItemDelegate(TagDelegate(self)) - def set_database(self, db, tag_match, popularity): + def set_database(self, db, tag_match, sort_by): self.hidden_categories = config['tag_browser_hidden_categories'] self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories, search_restriction=None) - self.popularity = popularity + self.sort_by = sort_by self.tag_match = tag_match self.db = db self.search_restriction = None @@ -120,8 +124,9 @@ class TagsView(QTreeView): # {{{ self.setContextMenuPolicy(Qt.CustomContextMenu) self.clicked.connect(self.toggle) self.customContextMenuRequested.connect(self.show_context_menu) - self.popularity.setChecked(config['sort_by_popularity']) - self.popularity.stateChanged.connect(self.sort_changed) + pop = config['sort_tags_by'] + self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop)) + self.sort_by.currentIndexChanged.connect(self.sort_changed) self.refresh_required.connect(self.recount, type=Qt.QueuedConnection) db.add_listener(self.database_changed) @@ -132,8 +137,8 @@ class TagsView(QTreeView): # {{{ def match_all(self): return self.tag_match and self.tag_match.currentIndex() > 0 - def sort_changed(self, state): - config.set('sort_by_popularity', state == Qt.Checked) + def sort_changed(self, pop): + config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop]) self.recount() def set_search_restriction(self, s): @@ -420,7 +425,7 @@ class TagsModel(QAbstractItemModel): # {{{ self.row_map = [] # get_node_tree cannot return None here, because row_map is empty - data = self.get_node_tree(config['sort_by_popularity']) + data = self.get_node_tree(config['sort_tags_by']) self.root_item = TagTreeItem() for i, r in enumerate(self.row_map): if self.hidden_categories and self.categories[i] in self.hidden_categories: @@ -464,11 +469,11 @@ class TagsModel(QAbstractItemModel): # {{{ # Now get the categories if self.search_restriction: - data = self.db.get_categories(sort_on_count=sort, + data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map, ids=self.db.search('', return_matches=True)) else: - data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map) + data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) tb_categories = self.db.field_metadata for category in tb_categories: @@ -482,7 +487,7 @@ class TagsModel(QAbstractItemModel): # {{{ return data def refresh(self): - data = self.get_node_tree(config['sort_by_popularity']) # get category data + data = self.get_node_tree(config['sort_tags_by']) # get category data if data is None: return False row_index = -1 @@ -691,7 +696,7 @@ class TagBrowserMixin(object): # {{{ def __init__(self, db): self.library_view.model().count_changed_signal.connect(self.tags_view.recount) self.tags_view.set_database(self.library_view.model().db, - self.tag_match, self.popularity) + self.tag_match, self.sort_by) self.tags_view.tags_marked.connect(self.search.search_from_tags) self.tags_view.tags_marked.connect(self.saved_search.clear_to_help) self.tags_view.tag_list_edit.connect(self.do_tags_list_edit) @@ -752,9 +757,13 @@ class TagBrowserWidget(QWidget): # {{{ parent.tags_view = TagsView(parent) self._layout.addWidget(parent.tags_view) - parent.popularity = QCheckBox(parent) - parent.popularity.setText(_('Sort by &popularity')) - self._layout.addWidget(parent.popularity) + parent.sort_by = QComboBox(parent) + # Must be in the same order as db2.CATEGORY_SORTS + for x in (_('Sort by name'), _('Sort by popularity'), + _('Sort by average rating')): + parent.sort_by.addItem(x) + parent.sort_by.setCurrentIndex(0) + self._layout.addWidget(parent.sort_by) parent.tag_match = QComboBox(parent) for x in (_('Match any'), _('Match all')): diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 2fb22a27f4..321a5fce17 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -64,6 +64,10 @@ class Tag(object): self.state = state self.avg_rating = avg/2.0 if avg is not None else 0 self.sort = sort + if self.avg_rating > 0: + if tooltip: + tooltip = tooltip + ': ' + tooltip = _('%sAverage rating is %3.1f')%(tooltip, self.avg_rating) self.tooltip = tooltip self.icon = icon @@ -687,7 +691,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): tn=field['table'], col=field['link_column']), (id_,)) return set(x[0] for x in ans) - def get_categories(self, sort_on_count=False, ids=None, icon_map=None): + CATEGORY_SORTS = ('name', 'popularity', 'rating') + + def get_categories(self, sort='name', ids=None, icon_map=None): self.books_list_filter.change([] if not ids else ids) categories = {} @@ -711,10 +717,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): else: query = '''SELECT id, {0}, count, avg_rating, sort FROM tag_browser_filtered_{1}'''.format(cn, tn) - if sort_on_count: - query += ' ORDER BY count DESC' - else: + if sort == 'popularity': + query += ' ORDER BY count DESC, sort ASC' + elif sort == 'name': query += ' ORDER BY sort ASC' + else: + query += ' ORDER BY avg_rating DESC, sort ASC' data = self.conn.get(query) # icon_map is not None if get_categories is to store an icon and @@ -770,11 +778,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if count > 0: categories['formats'].append(Tag(fmt, count=count, icon=icon)) - if sort_on_count: - categories['formats'].sort(cmp=lambda x,y:cmp(x.count, y.count), - reverse=True) - else: - categories['formats'].sort(cmp=lambda x,y:cmp(x.name, y.name)) + if sort == 'popularity': + categories['formats'].sort(key=lambda x: x.count, reverse=True) + else: # no ratings exist to sort on + categories['formats'].sort(key = lambda x:x.name) #### Now do the user-defined categories. #### user_categories = prefs['user_categories'] @@ -799,12 +806,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # Not a problem if we accumulate entries in the icon map if icon_map is not None: icon_map[cat_name] = icon_map[':user'] - if sort_on_count: + if sort == 'popularity': categories[cat_name] = \ - sorted(items, cmp=(lambda x, y: cmp(y.count, x.count))) + sorted(items, key=lambda x: x.count, reverse=True) + elif sort == 'name': + categories[cat_name] = \ + sorted(items, key=lambda x: x.sort.lower()) else: categories[cat_name] = \ - sorted(items, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower()))) + sorted(items, key=lambda x:x.avg_rating, reverse=True) #### Finally, the saved searches category #### items = [] From bb8bc9cea5a2bb725240adf54a5f2847b6701877 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 09:07:24 -0600 Subject: [PATCH 18/26] Display average rating by partial coloring of category icon --- resources/default_tweaks.py | 7 +- resources/images/series.svg | 2133 ++++++------ resources/images/user_profile.svg | 4988 ++--------------------------- src/calibre/gui2/tag_view.py | 59 +- 4 files changed, 1346 insertions(+), 5841 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 2075391da4..aaeb992151 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -72,9 +72,4 @@ gui_pubdate_display_format = 'MMM yyyy' # 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/resources/images/series.svg b/resources/images/series.svg index c26d1ef7a2..f2eb87b709 100644 --- a/resources/images/series.svg +++ b/resources/images/series.svg @@ -1,1096 +1,1071 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + id="linearGradient6642"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + 2007-06-23 + + + Lapo Calamandrei + + + + + address + book + contact + + + + + + + Andreas Nilsson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/user_profile.svg b/resources/images/user_profile.svg index 5a51783d7c..2fc0eea150 100644 --- a/resources/images/user_profile.svg +++ b/resources/images/user_profile.svg @@ -1,4750 +1,312 @@ + + version="1.1"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + inkscape:grid-bbox="true" + inkscape:document-units="px" + fill="#9db029" + stroke="#727e0a" + inkscape:window-width="1330" + inkscape:window-height="815" + inkscape:window-x="202" + inkscape:window-y="68" + inkscape:window-maximized="0" /> + id="metadata4"> image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + user + person + + + + + + + + + + + - - + inkscape:label="cipek" + inkscape:groupmode="layer" + style="display:inline"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + style="opacity:1;color:#000000;fill:url(#linearGradient1372);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + d="M 13.365469,24.850231 L 19.043607,24.850231 L 15.731361,21.774572 L 15.021593,22.720929 L 14.311825,22.011162 L 13.365469,24.850231 z " + id="path4173" /> - + sodipodi:nodetypes="cccc" + id="path4370" + d="M 19.882923,32.490544 C 21.530768,31.712992 22.297815,29.810737 22.297815,29.810737 C 21.014177,24.39981 16.976336,20.652646 16.976336,20.652646 C 16.976336,20.652646 20.274824,29.141269 19.882923,32.490544 z " + style="opacity:0.22784807;color:#000000;fill:url(#linearGradient1366);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" /> + id="layer2" + inkscape:label="dalsi cipek" + style="display:inline"> + + + + + + + + + diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 9cc90ca83f..70d626bc6e 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -13,11 +13,10 @@ from functools import partial from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \ QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, \ - QPushButton, QWidget, QItemDelegate, QString, QPen, \ - QColor, QLinearGradient, QBrush + QPushButton, QWidget, QItemDelegate, QString from calibre.gui2 import config, NONE -from calibre.utils.config import prefs, tweaks +from calibre.utils.config import prefs from calibre.library.field_metadata import TagsIcons from calibre.utils.search_query_parser import saved_searches from calibre.gui2 import error_dialog @@ -38,51 +37,25 @@ class TagDelegate(QItemDelegate): 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) + painter.save() + if item.tag.state != 0 or not config['show_avg_rating']: + icon.paint(painter, r, Qt.AlignLeft) + else: + icon.paint(painter, r, Qt.AlignLeft, mode=QIcon.Disabled) + rating = item.tag.avg_rating + if rating is None: + rating = 5.0 + painter.setClipRect(r.left(), r.bottom()-int(r.height()*(rating/5.0)), + r.width(), r.height()) + icon.paint(painter, r, Qt.AlignLeft) + painter.setClipRect(r) - # 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 + r.setLeft(r.left()+r.height()+3) painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, QString('[%d] %s'%(item.tag.count, item.tag.name))) + painter.restore() class TagsView(QTreeView): # {{{ From 82bb29424f5db4accc58c0f04cb1bb6ac3d2335a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 09:13:24 -0600 Subject: [PATCH 19/26] Cleanup Tag browser text display --- src/calibre/gui2/tag_view.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 70d626bc6e..fc20d98279 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -13,7 +13,7 @@ from functools import partial from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \ QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, \ - QPushButton, QWidget, QItemDelegate, QString + QPushButton, QWidget, QItemDelegate from calibre.gui2 import config, NONE from calibre.utils.config import prefs @@ -37,15 +37,15 @@ class TagDelegate(QItemDelegate): QItemDelegate.paint(self, painter, option, index) return r = option.rect - icon = self._parent.model().data(index, Qt.DecorationRole).toPyObject() + model = self._parent.model() + icon = model.data(index, Qt.DecorationRole).toPyObject() painter.save() - if item.tag.state != 0 or not config['show_avg_rating']: + if item.tag.state != 0 or not config['show_avg_rating'] or \ + item.tag.avg_rating is None: icon.paint(painter, r, Qt.AlignLeft) else: icon.paint(painter, r, Qt.AlignLeft, mode=QIcon.Disabled) rating = item.tag.avg_rating - if rating is None: - rating = 5.0 painter.setClipRect(r.left(), r.bottom()-int(r.height()*(rating/5.0)), r.width(), r.height()) icon.paint(painter, r, Qt.AlignLeft) @@ -54,7 +54,7 @@ class TagDelegate(QItemDelegate): # Paint the text r.setLeft(r.left()+r.height()+3) painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, - QString('[%d] %s'%(item.tag.count, item.tag.name))) + model.data(index, Qt.DisplayRole).toString()) painter.restore() class TagsView(QTreeView): # {{{ @@ -345,11 +345,7 @@ class TagTreeItem(object): # {{{ if self.tag.count == 0: return QVariant('%s'%(self.tag.name)) else: - 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)) + return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) if role == Qt.EditRole: return QVariant(self.tag.name) if role == Qt.DecorationRole: From 7445bae81fa03579c74c8537b78011624e476a8f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 09:29:29 -0600 Subject: [PATCH 20/26] ... --- src/calibre/gui2/tag_view.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index fc20d98279..ce37b44f79 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -26,18 +26,13 @@ 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 - model = self._parent.model() + model = self.parent().model() icon = model.data(index, Qt.DecorationRole).toPyObject() painter.save() if item.tag.state != 0 or not config['show_avg_rating'] or \ From 1efe9d8d1d1a50de04b2d471e385d256618374ef Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 09:51:11 -0600 Subject: [PATCH 21/26] Better rendering of avg rating indication --- src/calibre/gui2/tag_view.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index ce37b44f79..5e5393fdc4 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -39,7 +39,9 @@ class TagDelegate(QItemDelegate): item.tag.avg_rating is None: icon.paint(painter, r, Qt.AlignLeft) else: - icon.paint(painter, r, Qt.AlignLeft, mode=QIcon.Disabled) + painter.setOpacity(0.3) + icon.paint(painter, r, Qt.AlignLeft) + painter.setOpacity(1) rating = item.tag.avg_rating painter.setClipRect(r.left(), r.bottom()-int(r.height()*(rating/5.0)), r.width(), r.height()) From 4b50654ef2ec7e28dae0426d9a07d7c745e067d0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 10:26:10 -0600 Subject: [PATCH 22/26] Fix #5845 (Updated recipe for Danas) --- resources/recipes/danas.recipe | 56 +++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/resources/recipes/danas.recipe b/resources/recipes/danas.recipe index d82928e323..159553370a 100644 --- a/resources/recipes/danas.recipe +++ b/resources/recipes/danas.recipe @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- __license__ = 'GPL v3' __copyright__ = '2008-2010, Darko Miletic ' ''' @@ -23,7 +22,14 @@ class Danas(BasicNewsRecipe): language = 'sr' publication_type = 'newspaper' remove_empty_feeds = True - extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} .article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif} .nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif} .antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small} .antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} ' + extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} + @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} + .article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif} + .nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif} + .antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; + margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small} + .antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; + font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} """ conversion_options = { 'comment' : description @@ -42,19 +48,32 @@ class Danas(BasicNewsRecipe): ] feeds = [ - (u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27') - ,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' ) - ,(u'Dru\xc5\xa1tvo', u'http://www.danas.rs/rss/rss.asp?column_id=24') - ,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' ) - ,(u'Ekonomija', u'http://www.danas.rs/rss/rss.asp?column_id=6' ) - ,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25') - ,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28') - ,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' ) - ,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13') - ,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42') - ,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19') - ,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' ) - ,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47') + (u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27') + ,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' ) + ,(u'Drustvo' , u'http://www.danas.rs/rss/rss.asp?column_id=24') + ,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' ) + ,(u'Ekonomija' , u'http://www.danas.rs/rss/rss.asp?column_id=6' ) + ,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25') + ,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28') + ,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' ) + ,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13') + ,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42') + ,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19') + ,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' ) + ,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47') + ,(u'Sluzbena beleska' , u'http://www.danas.rs/rss/rss.asp?column_id=48') + ,(u'Suocavanja' , u'http://www.danas.rs/rss/rss.asp?column_id=49') + ,(u'Moj Izbor' , u'http://www.danas.rs/rss/rss.asp?column_id=50') + ,(u'Direktno' , u'http://www.danas.rs/rss/rss.asp?column_id=51') + ,(u'I tome slicno' , u'http://www.danas.rs/rss/rss.asp?column_id=52') + ,(u'No longer and not yet', u'http://www.danas.rs/rss/rss.asp?column_id=53') + ,(u'Resetovanje' , u'http://www.danas.rs/rss/rss.asp?column_id=54') + ,(u'Iza scene' , u'http://www.danas.rs/rss/rss.asp?column_id=60') + ,(u'Drustvoslovlje' , u'http://www.danas.rs/rss/rss.asp?column_id=55') + ,(u'Zvaka u pepeljari' , u'http://www.danas.rs/rss/rss.asp?column_id=56') + ,(u'Vostani Serbie' , u'http://www.danas.rs/rss/rss.asp?column_id=57') + ,(u'Med&Jad-a' , u'http://www.danas.rs/rss/rss.asp?column_id=58') + ,(u'Svetlosti pozornice' , u'http://www.danas.rs/rss/rss.asp?column_id=59') ] def preprocess_html(self, soup): @@ -65,3 +84,10 @@ class Danas(BasicNewsRecipe): def print_version(self, url): return url + '&action=print' + def get_cover_url(self): + cover_url = None + soup = self.index_to_soup('http://www.danas.rs/') + for citem in soup.findAll('img'): + if citem['src'].endswith('naslovna.jpg'): + return 'http://www.danas.rs' + citem['src'] + return cover_url From 60219fe54cd83e3ce53ebd21ca985b6aa62f044b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 10:37:27 -0600 Subject: [PATCH 23/26] Add author_sort_map to MetaInformation --- src/calibre/ebooks/metadata/__init__.py | 7 ++++++- src/calibre/ebooks/metadata/book/__init__.py | 2 ++ src/calibre/ebooks/metadata/book/base.py | 1 + src/calibre/library/database2.py | 6 +++++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index c633e5149b..8caca1f261 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -223,6 +223,7 @@ class MetaInformation(object): 'isbn', 'tags', 'cover_data', 'application_id', 'guide', 'manifest', 'spine', 'toc', 'cover', 'language', 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', + 'author_sort_map', 'pubdate', 'rights', 'publication_type', 'uuid'): if hasattr(mi, attr): setattr(ans, attr, getattr(mi, attr)) @@ -244,6 +245,7 @@ class MetaInformation(object): self.tags = getattr(mi, 'tags', []) #: mi.cover_data = (ext, data) self.cover_data = getattr(mi, 'cover_data', (None, None)) + self.author_sort_map = getattr(mi, 'author_sort_map', {}) for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher', 'series', 'series_index', 'rating', 'isbn', 'language', @@ -258,7 +260,7 @@ class MetaInformation(object): 'series', 'series_index', 'tags', 'rating', 'isbn', 'language', 'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover', 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', - 'rights', 'publication_type', 'uuid' + 'rights', 'publication_type', 'uuid', 'author_sort_map' ): prints(x, getattr(self, x, 'None')) @@ -288,6 +290,9 @@ class MetaInformation(object): self.tags += mi.tags self.tags = list(set(self.tags)) + if mi.author_sort_map: + self.author_sort_map.update(mi.author_sort_map) + if getattr(mi, 'cover_data', False): other_cover = mi.cover_data[-1] self_cover = self.cover_data[-1] if self.cover_data else '' diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py index 8483c2bddb..c3b95f1188 100644 --- a/src/calibre/ebooks/metadata/book/__init__.py +++ b/src/calibre/ebooks/metadata/book/__init__.py @@ -35,6 +35,8 @@ PUBLICATION_METADATA_FIELDS = frozenset([ 'title_sort', # Ordered list of authors. Must never be None, can be [_('Unknown')] 'authors', + # Map of sort strings for each author + 'author_sort_map', # Pseudo field that can be set, but if not set is auto generated # from authors and languages 'author_sort', diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index ad5dd17ace..ba34f04f95 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -16,6 +16,7 @@ NULL_VALUES = { 'classifiers' : {}, 'languages' : [], 'device_collections': [], + 'author_sort_map': {}, 'authors' : [_('Unknown')], 'title' : _('Unknown'), } diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 321a5fce17..c7830187df 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -433,7 +433,11 @@ 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) + if mi.authors: + mi.author_sort_map = {} + for name, sort in zip(mi.authors, self.authors_sort_strings(idx, + index_is_id)): + mi.author_sort_map[name] = sort 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) From 6d80e2093cee088427e2d5fd50099f2fdc0bacb4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 11:03:19 -0600 Subject: [PATCH 24/26] Thai Rath and The Nation (Thailand) by Anat Ruangrassamee --- resources/recipes/thairath.recipe | 58 ++++++++++++++++++++++++ resources/recipes/the_nation_thai.recipe | 44 ++++++++++++++++++ src/calibre/utils/localization.py | 1 + 3 files changed, 103 insertions(+) create mode 100644 resources/recipes/thairath.recipe create mode 100644 resources/recipes/the_nation_thai.recipe diff --git a/resources/recipes/thairath.recipe b/resources/recipes/thairath.recipe new file mode 100644 index 0000000000..6ebb84f3a5 --- /dev/null +++ b/resources/recipes/thairath.recipe @@ -0,0 +1,58 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1271637235(BasicNewsRecipe): + + title = u'Thairath' + __author__ = 'Anat R.' + language = 'th' + + oldest_article = 7 + + max_articles_per_feed = 100 + no_stylesheets = True + + remove_javascript = True + + use_embedded_content = False + feeds = [(u'News', +u'http://www.thairath.co.th/rss/news.xml'), (u'Politics', +u'http://www.thairath.co.th/rss/pol.xml'), (u'Economy', +u'http://www.thairath.co.th/rss/eco.xml'), (u'International', +u'http://www.thairath.co.th/rss/oversea.xml'), (u'Sports', +u'http://www.thairath.co.th/rss/sport.xml'), (u'Life', +u'http://www.thairath.co.th/rss/life.xml'), (u'Education', +u'http://www.thairath.co.th/rss/edu.xml'), (u'Tech', +u'http://www.thairath.co..th/rss/tech.xml'), (u'Entertainment', +u'http://www.thairath.co.th/rss/ent.xml')] + keep_only_tags = [] + + keep_only_tags.append(dict(name = 'h1', attrs = {'id' : 'title'})) + + keep_only_tags.append(dict(name = 'ul', attrs = {'class' : +'detail-info'})) + + keep_only_tags.append(dict(name = 'img', attrs = {'class' : +'detail-image'})) + + keep_only_tags.append(dict(name = 'div', attrs = {'class' : +'entry'})) + remove_tags = [] + remove_tags.append(dict(name = 'div', attrs = {'id': +'menu-holder'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': +'addthis_toolbox addthis_default_style'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': 'box top-item'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': 'column-200 column-margin-430'})) + + remove_tags.append(dict(name = 'div', attrs = {'id': +'detail-related'})) + + remove_tags.append(dict(name = 'div', attrs = {'id': 'related'})) + + remove_tags.append(dict(name = 'id', attrs = {'class': 'footer'})) + + remove_tags.append(dict(name = "ul",attrs = +{'id':'banner-highlights-images'})) diff --git a/resources/recipes/the_nation_thai.recipe b/resources/recipes/the_nation_thai.recipe new file mode 100644 index 0000000000..a33a16e0a5 --- /dev/null +++ b/resources/recipes/the_nation_thai.recipe @@ -0,0 +1,44 @@ + +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1271596863(BasicNewsRecipe): + + title = u'The Nation' + __author__ = 'Anat R.' + language = 'en_TH' + + oldest_article = 7 + + max_articles_per_feed = 100 + no_stylesheets = True + + remove_javascript = True + + use_embedded_content = False + feeds = [(u'Topstory', +u'http://www.nationmultimedia.com/home/rss/topstories.rss'), +(u'National', u'http://www.nationmultimedia.com/home/rss/national.rss'), + (u'Politics', +u'http://www.nationmultimedia.com/home/rss/politics.rss'), (u'Business', + u'http://www.nationmultimedia.com/home/rss/business.rss'), +(u'Regional', u'http://www.nationmultimedia.com/home/rss/regional.rss'), + (u'Sports', u'http://www.nationmultimedia.com/home/rss/sport.rss'), +(u'Travel', u'http://www.nationmultimedia.com/home/rss/travel.rss'), +(u'Life', u'http://www.nationmultimedia.com/home/rss/life.rss')] + keep_only_tags = [] + + keep_only_tags.append(dict(name = 'div', attrs = {'class' : +'pd10'})) + remove_tags = [] + + remove_tags.append(dict(name = 'div', attrs = {'class': +'WrapperHeaderCol2-2'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': +'LayoutMenu2'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': +'TextHeaderRight'})) + + remove_tags.append(dict(name = "ul",attrs = {'id':'toolZoom'})) + diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index 026547ee2e..e60a3233c6 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -103,6 +103,7 @@ _extra_lang_codes = { 'en_TH' : _('English (Thailand)'), 'en_CY' : _('English (Cyprus)'), 'en_PK' : _('English (Pakistan)'), + 'en_IL' : _('English (Israel)'), 'en_SG' : _('English (Singapore)'), 'en_YE' : _('English (Yemen)'), 'en_IE' : _('English (Ireland)'), From 9e31b075da56b77217eda794ce39b5415f4ccf41 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 11:06:21 -0600 Subject: [PATCH 25/26] Click to open in book details on device view now opens the actual file instead of the folder containing it --- src/calibre/gui2/book_details.py | 5 +---- src/calibre/gui2/status.py | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 75c045d011..8cf726420c 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -258,8 +258,7 @@ class BookDetails(QWidget): id_, fmt = val.split(':') self.view_specific_format.emit(int(id_), fmt) elif typ == 'devpath': - path = os.path.dirname(val) - QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + QDesktopServices.openUrl(QUrl.fromLocalFile(val)) def mouseReleaseEvent(self, ev): @@ -275,8 +274,6 @@ class BookDetails(QWidget): self.setToolTip('

'+_('Click to open Book Details window') + '

' + _('Path') + ': ' + data.get(_('Path'), '')) - - def reset_info(self): self.show_data({}) diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py index 90426f8021..9aa9b8262c 100644 --- a/src/calibre/gui2/status.py +++ b/src/calibre/gui2/status.py @@ -239,8 +239,7 @@ class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface): id_, fmt = val.split(':') self.view_specific_format.emit(int(id_), fmt) elif typ == 'devpath': - path = os.path.dirname(val) - QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + QDesktopServices.openUrl(QUrl.fromLocalFile(val)) def resizeEvent(self, ev): From 6db976e5aaf973ffa72b6a6a0dac5fe6748a6258 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 15 Jun 2010 11:16:09 -0600 Subject: [PATCH 26/26] Fix #5843 (Book covers not displaying when in device view) --- src/calibre/gui2/book_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 8cf726420c..a397ab903d 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -109,7 +109,7 @@ class CoverView(QWidget): # {{{ def show_data(self, data): self.animation.stop() - if data.get('id', None) == self.data.get('id', None): + if data.get('id', True) == self.data.get('id', False): return self.data = {'id':data.get('id', None)} if data.has_key('cover'):