Add support for adding custom news profiles to the GUI

This commit is contained in:
Kovid Goyal 2008-01-24 00:10:19 +00:00
parent 7a3cd084a5
commit 2fa7063034
12 changed files with 5472 additions and 28 deletions

View File

@ -104,16 +104,20 @@ def process_profile(args, options, logger=None):
if len(args) < 2:
args.append(name)
args[1] = name
index = -1
if len(args) == 2:
try:
index = -1
if args[1] != 'default':
index = available_profiles.index(args[1])
if isinstance(args[1], basestring):
if args[1] != 'default':
index = available_profiles.index(args[1])
except ValueError:
raise CommandLineError('Unknown profile: %s\nValid profiles: %s'%(args[1], available_profiles))
else:
raise CommandLineError('Only one profile at a time is allowed.')
profile = DefaultProfile if index == -1 else builtin_profiles[index]
if isinstance(args[1], basestring):
profile = DefaultProfile if index == -1 else builtin_profiles[index]
else:
profile = args[1]
profile = profile(logger, options.verbose, options.username, options.password)
if profile.browser is not None:
options.browser = profile.browser
@ -170,7 +174,11 @@ def process_profile(args, options, logger=None):
def main(args=sys.argv, logger=None):
parser = option_parser()
options, args = parser.parse_args(args)
if not isinstance(args[-1], basestring): # Called from GUI
options, args2 = parser.parse_args(args[:-1])
args = args2 + [args[-1]]
else:
options, args = parser.parse_args(args)
if len(args) > 2 or (len(args) == 1 and not options.user_profile):
parser.print_help()
return 1

View File

@ -37,6 +37,7 @@ class DefaultProfile(object):
url_search_order = ['guid', 'link'] # THe order of elements to search for a URL when parssing the RSS feed
pubdate_fmt = None # The format string used to parse the publication date in the RSS feed. If set to None some default heuristics are used, these may fail, in which case set this to the correct string or re-implement strptime in your subclass.
use_pubdate = True, # If True will look for a publication date for each article. If False assumes the publication date is the current time.
summary_length = 500 # Max number of characters in the short description (ignored in DefaultProfile)
no_stylesheets = False # Download stylesheets only if False
allow_duplicates = False # If False articles with the same title in the same feed are not downloaded multiple times
needs_subscription = False # If True the GUI will ask the userfor a username and password to use while downloading
@ -53,13 +54,16 @@ class DefaultProfile(object):
# See the built-in profiles for examples of these settings.
feeds = []
def get_feeds(self):
'''
Return a list of RSS feeds to fetch for this profile. Each element of the list
must be a 2-element tuple of the form (title, url).
'''
raise NotImplementedError
if not self.feeds:
raise NotImplementedError
return self.feeds
@classmethod
def print_version(cls, url):
@ -225,7 +229,6 @@ class DefaultProfile(object):
added_articles[title].append(d['title'])
if delta > self.oldest_article*3600*24:
continue
except Exception, err:
if self.verbose:
self.logger.exception('Error parsing article:\n%s'%(item,))
@ -325,19 +328,16 @@ class FullContentProfile(DefaultProfile):
This profile is designed for feeds that embed the full article content in the RSS file.
'''
summary_length = 500 # Max number of characters in the short description
max_recursions = 0
article_counter = 0
html_description = True
def build_index(self):
'''Build an RSS based index.html'''
import os
articles = self.parse_feeds(require_url=False)
def build_sub_index(title, items):
ilist = ''
li = u'<li><a href="%(url)s">%(title)s</a> <span style="font-size: x-small">[%(date)s]</span><br/>\n'+\
@ -347,7 +347,7 @@ class FullContentProfile(DefaultProfile):
if not content:
self.logger.debug('Skipping article as it has no content:%s'%item['title'])
continue
item['description'] = item['description'][:self.summary_length]+'&hellip;'
item['description'] = cutoff(item['description'], self.summary_length)+'&hellip;'
self.article_counter = self.article_counter + 1
url = os.path.join(self.temp_dir, 'article%d.html'%self.article_counter)
item['url'] = url
@ -384,6 +384,7 @@ class FullContentProfile(DefaultProfile):
clist += u'<li><a href="%s">%s</a></li>\n'%(prefix+cfile, category)
src = build_sub_index(category, articles[category])
open(cfile, 'wb').write(src.encode('utf-8'))
open('/tmp/category'+str(cnum)+'.html', 'wb').write(src.encode('utf-8'))
src = '''\
<html>
@ -401,4 +402,15 @@ class FullContentProfile(DefaultProfile):
open(index, 'wb').write(src.encode('utf-8'))
return index
def cutoff(src, pos, fuzz=50):
si = src.find(';', pos)
if si > 0 and si-pos > fuzz:
si = -1
gi = src.find('>', pos)
if gi > 0 and gi-pos > fuzz:
gi = -1
npos = max(si, gi)
if npos < 0:
npos = pos
return src[:npos+1]

View File

@ -23,8 +23,6 @@ Fetch Dilbert.
from libprs500.ebooks.lrf.web.profiles import DefaultProfile
import re
class Dilbert(DefaultProfile):
title = 'Dilbert'

View File

@ -0,0 +1,187 @@
## Copyright (C) 2008 Kovid Goyal kovid@kovidgoyal.net
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import time
from PyQt4.QtCore import SIGNAL
from PyQt4.QtGui import QDialog, QMessageBox
from libprs500.ebooks.lrf.web.profiles import FullContentProfile, DefaultProfile
from libprs500.gui2.dialogs.user_profiles_ui import Ui_Dialog
from libprs500.gui2 import qstring_to_unicode, error_dialog, question_dialog
class UserProfiles(QDialog, Ui_Dialog):
def __init__(self, parent, feeds):
QDialog.__init__(self, parent)
Ui_Dialog.__init__(self)
self.setupUi(self)
self.connect(self.remove_feed_button, SIGNAL('clicked(bool)'),
self.added_feeds.remove_selected_items)
self.connect(self.remove_profile_button, SIGNAL('clicked(bool)'),
self.available_profiles.remove_selected_items)
self.connect(self.add_feed_button, SIGNAL('clicked(bool)'),
self.add_feed)
self.connect(self.add_profile_button, SIGNAL('clicked(bool)'),
self.add_profile)
self.connect(self.feed_url, SIGNAL('returnPressed()'), self.add_feed)
self.connect(self.feed_title, SIGNAL('returnPressed()'), self.add_feed)
self.connect(self.available_profiles,
SIGNAL('currentItemChanged(QListWidgetItem*, QListWidgetItem*)'),
self.edit_profile)
self.connect(self.toggle_mode_button, SIGNAL('clicked(bool)'), self.toggle_mode)
self.clear()
for title, src in feeds:
self.available_profiles.add_item(title, (title, src), replace=True)
def edit_profile(self, current, previous):
if not current:
current = previous
src = current.user_data[1]
if 'class BasicUserProfile' in src:
profile = self.create_class(src)
self.populate_options(profile)
self.stacks.setCurrentIndex(0)
self.toggle_mode_button.setText('Switch to Advanced mode')
else:
self.source_code.setPlainText(src)
self.stacks.setCurrentIndex(1)
self.toggle_mode_button.setText('Switch to Basic mode')
def toggle_mode(self, *args):
if self.stacks.currentIndex() == 1:
self.stacks.setCurrentIndex(0)
self.toggle_mode_button.setText('Switch to Advanced mode')
else:
self.stacks.setCurrentIndex(1)
self.toggle_mode_button.setText('Switch to Basic mode')
if not qstring_to_unicode(self.source_code.toPlainText()).strip():
src = self.options_to_profile()[0]
self.source_code.setPlainText(src.replace('BasicUserProfile', 'AdvancedUserProfile'))
def add_feed(self, *args):
title = qstring_to_unicode(self.feed_title.text()).strip()
if not title:
d = error_dialog(self, 'Feed must have a title', 'The feed must have a title')
d.exec_()
return
url = qstring_to_unicode(self.feed_url.text()).strip()
if not url:
d = error_dialog(self, 'Feed must have a URL', 'The feed %s must have a URL'%title)
d.exec_()
return
try:
self.added_feeds.add_item(title+' - '+url, (title, url))
except ValueError:
error_dialog(self, 'Already in list', 'This feed has already been added to the profile').exec_()
return
self.feed_title.setText('')
self.feed_url.setText('')
def options_to_profile(self):
classname = 'BasicUserProfile'+str(int(time.time()))
title = qstring_to_unicode(self.profile_title.text()).strip()
if not title:
title = classname
self.profile_title.setText(title)
summary_length = self.summary_length.value()
oldest_article = self.oldest_article.value()
max_articles = self.max_articles.value()
feeds = [i.user_data for i in self.added_feeds.items()]
src = '''\
class %(classname)s(%(base_class)s):
title = %(title)s
summary_length = %(summary_length)d
oldest_article = %(oldest_article)d
max_articles_per_feed = %(max_articles)d
feeds = %(feeds)s
'''%dict(classname=classname, title=repr(title), summary_length=summary_length,
feeds=repr(feeds), oldest_article=oldest_article,
max_articles=max_articles,
base_class='DefaultProfile' if self.full_articles.isChecked() else 'FullContentProfile')
return src, title
def populate_source_code(self):
src = self.options_to_profile().replace('BasicUserProfile', 'AdvancedUserProfile')
self.source_code.setPlainText(src)
@classmethod
def create_class(cls, src):
environment = {'FullContentProfile':FullContentProfile, 'DefaultProfile':DefaultProfile}
exec src in environment
for item in environment.values():
if hasattr(item, 'build_index'):
if item.__name__ not in ['DefaultProfile', 'FullContentProfile']:
return item
def add_profile(self, clicked):
if self.stacks.currentIndex() == 0:
src, title = self.options_to_profile()
try:
self.create_class(src)
except Exception, err:
error_dialog(self, 'Invalid input',
'<p>Could not create profile. Error:<br>%s'%str(err)).exec_()
return
profile = src
else:
src = qstring_to_unicode(self.source_code.toPlainText())
try:
title = self.create_class(src).title
except Exception, err:
error_dialog(self, 'Invalid input',
'<p>Could not create profile. Error:<br>%s'%str(err)).exec_()
return
profile = src.replace('BasicUserProfile', 'AdvancedUserProfile')
try:
self.available_profiles.add_item(title, (title, profile), replace=False)
except ValueError:
d = question_dialog(self, 'Replace profile?',
'A custom profile named %s already exists. Do you want to replace it?'%title)
if d.exec_() == QMessageBox.Yes:
self.available_profiles.add_item(title, (title, profile), replace=True)
else:
return
self.clear()
def populate_options(self, profile):
self.oldest_article.setValue(profile.oldest_article)
self.max_articles.setValue(profile.max_articles_per_feed)
self.summary_length.setValue(profile.summary_length)
self.profile_title.setText(profile.title)
self.added_feeds.clear()
for title, url in profile.feeds:
self.added_feeds.add_item(title+' - '+url, (title, url))
self.feed_title.setText('')
self.feed_url.setText('')
self.full_articles.setChecked(isinstance(profile, FullContentProfile))
def clear(self):
self.populate_options(FullContentProfile)
self.source_code.setText('')
def profiles(self):
for i in self.available_profiles.items():
yield i.user_data

View File

@ -0,0 +1,382 @@
<ui version="4.0" >
<class>Dialog</class>
<widget class="QDialog" name="Dialog" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>703</width>
<height>661</height>
</rect>
</property>
<property name="windowTitle" >
<string>Add custom RSS feed</string>
</property>
<property name="windowIcon" >
<iconset resource="../images.qrc" >:/images/user_profile.svg</iconset>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="QGroupBox" name="groupBox" >
<property name="title" >
<string>Available user profiles</string>
</property>
<layout class="QVBoxLayout" >
<item>
<widget class="BasicList" name="available_profiles" >
<property name="selectionMode" >
<enum>QAbstractItemView::MultiSelection</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="add_profile_button" >
<property name="text" >
<string>Add/Update &amp;profile</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >:/images/plus.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="remove_profile_button" >
<property name="text" >
<string>&amp;Remove profile</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >:/images/list_remove.svg</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" >
<layout class="QVBoxLayout" >
<item>
<widget class="QPushButton" name="toggle_mode_button" >
<property name="text" >
<string>Switch to Advanced mode</string>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="stacks" >
<property name="currentIndex" >
<number>0</number>
</property>
<widget class="QWidget" name="page" >
<layout class="QVBoxLayout" >
<item>
<widget class="QLabel" name="label" >
<property name="text" >
<string>&lt;html>&lt;head>&lt;meta name="qrichtext" content="1" />&lt;style type="text/css">
p, li { white-space: pre-wrap; }
&lt;/style>&lt;/head>&lt;body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal;">
&lt;p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Create a basic news profile, by adding RSS feeds to it. &lt;br />For most feeds, you will have to use the "Advanced" setting to further customize the fetch process.&lt;br />The Basic tab is useful mainly for feeds that have the full article content embedded within them.&lt;/p>&lt;/body>&lt;/html></string>
</property>
<property name="textFormat" >
<enum>Qt::RichText</enum>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="QLabel" name="label_2" >
<property name="text" >
<string>Profile &amp;title:</string>
</property>
<property name="buddy" >
<cstring>profile_title</cstring>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2" >
<widget class="QLineEdit" name="profile_title" >
<property name="font" >
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2" >
<widget class="QLabel" name="label_3" >
<property name="text" >
<string>&amp;Summary length:</string>
</property>
<property name="buddy" >
<cstring>summary_length</cstring>
</property>
</widget>
</item>
<item row="4" column="2" >
<widget class="QSpinBox" name="summary_length" >
<property name="suffix" >
<string> characters</string>
</property>
<property name="minimum" >
<number>0</number>
</property>
<property name="maximum" >
<number>100000</number>
</property>
<property name="value" >
<number>500</number>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_6" >
<property name="text" >
<string>&amp;Oldest article:</string>
</property>
<property name="buddy" >
<cstring>oldest_article</cstring>
</property>
</widget>
</item>
<item row="2" column="2" >
<widget class="QSpinBox" name="oldest_article" >
<property name="toolTip" >
<string>The oldest article to download</string>
</property>
<property name="suffix" >
<string> days</string>
</property>
<property name="minimum" >
<number>1</number>
</property>
<property name="maximum" >
<number>365</number>
</property>
<property name="value" >
<number>7</number>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_7" >
<property name="text" >
<string>&amp;Max. number of articles per feed:</string>
</property>
<property name="buddy" >
<cstring>max_articles</cstring>
</property>
</widget>
</item>
<item row="3" column="2" >
<widget class="QSpinBox" name="max_articles" >
<property name="toolTip" >
<string>Maximum number of articles to download per feed.</string>
</property>
<property name="minimum" >
<number>5</number>
</property>
<property name="maximum" >
<number>100</number>
</property>
<property name="value" >
<number>10</number>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QCheckBox" name="full_articles" >
<property name="toolTip" >
<string>Try to follow links in the RSS feed to full articles on the web. If you enable this option, you're probably going to end up having to use the advanced mode.</string>
</property>
<property name="text" >
<string>Try to download &amp;full articles</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2" >
<property name="title" >
<string>Feeds in profile</string>
</property>
<layout class="QHBoxLayout" >
<item>
<widget class="BasicList" name="added_feeds" >
<property name="selectionMode" >
<enum>QAbstractItemView::MultiSelection</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove_feed_button" >
<property name="toolTip" >
<string>Remove feed from profile</string>
</property>
<property name="text" >
<string>...</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >:/images/list_remove.svg</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3" >
<property name="title" >
<string>Add feed to profile</string>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="QLabel" name="label_4" >
<property name="text" >
<string>&amp;Feed title:</string>
</property>
<property name="buddy" >
<cstring>feed_title</cstring>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="QLineEdit" name="feed_title" />
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_5" >
<property name="text" >
<string>Feed &amp;URL:</string>
</property>
<property name="buddy" >
<cstring>feed_url</cstring>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QLineEdit" name="feed_url" />
</item>
<item row="2" column="0" colspan="2" >
<widget class="QPushButton" name="add_feed_button" >
<property name="toolTip" >
<string>Add feed to profile</string>
</property>
<property name="text" >
<string>&amp;Add feed</string>
</property>
<property name="icon" >
<iconset resource="../images.qrc" >:/images/plus.svg</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2" >
<layout class="QVBoxLayout" >
<item>
<widget class="QLabel" name="label_8" >
<property name="text" >
<string>For help with writing advanced news profiles, please visit &lt;a href="https://libprs500.kovidgoyal.net/wiki/UserProfiles">UserProfiles&lt;/a></string>
</property>
<property name="wordWrap" >
<bool>true</bool>
</property>
<property name="openExternalLinks" >
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4" >
<property name="title" >
<string>Profile source code (python)</string>
</property>
<layout class="QVBoxLayout" >
<item>
<widget class="QTextEdit" name="source_code" >
<property name="font" >
<font>
<family>DejaVu Sans Mono</family>
</font>
</property>
<property name="lineWrapMode" >
<enum>QTextEdit::NoWrap</enum>
</property>
<property name="acceptRichText" >
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2" >
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<set>QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BasicList</class>
<extends>QListWidget</extends>
<header>widgets.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../images.qrc" />
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<x>446</x>
<y>649</y>
</hint>
<hint type="destinationlabel" >
<x>0</x>
<y>632</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<x>175</x>
<y>643</y>
</hint>
<hint type="destinationlabel" >
<x>176</x>
<y>636</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -3,6 +3,7 @@
<file>images/back.svg</file>
<file>images/book.svg</file>
<file>images/search.svg</file>
<file>images/user_profile.svg</file>
<file>images/chapters.svg</file>
<file>images/clear_left.svg</file>
<file>images/config.svg</file>

View File

@ -2902,7 +2902,7 @@
<defs
id="defs135" />
<use
xlink:href="#XMLID_34_"
id="use138"
x="0"
y="0"
@ -2911,7 +2911,7 @@
<clipPath
id="XMLID_215_">
<use
xlink:href="#XMLID_34_"
id="use141"
x="0"
y="0"

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 156 KiB

View File

@ -48,6 +48,7 @@ from libprs500.gui2.dialogs.conversion_error import ConversionErrorDialog
from libprs500.gui2.dialogs.lrf_single import LRFSingleDialog
from libprs500.gui2.dialogs.config import ConfigDialog
from libprs500.gui2.dialogs.search import SearchDialog
from libprs500.gui2.dialogs.user_profiles import UserProfiles
from libprs500.gui2.lrf_renderer.main import file_renderer
from libprs500.gui2.lrf_renderer.main import option_parser as lrfviewerop
from libprs500.library.database import DatabaseLocked
@ -129,7 +130,7 @@ class Main(MainWindow, Ui_MainWindow):
QObject.connect(self.action_view, SIGNAL("triggered(bool)"), self.view_book)
self.action_sync.setMenu(sm)
self.action_edit.setMenu(md)
self.news_menu = NewsMenu()
self.news_menu = NewsMenu(self.customize_feeds)
self.action_news.setMenu(self.news_menu)
QObject.connect(self.news_menu, SIGNAL('fetch_news(PyQt_PyObject)'), self.fetch_news)
cm = QMenu()
@ -179,6 +180,8 @@ class Main(MainWindow, Ui_MainWindow):
self.device_detected, Qt.QueuedConnection)
self.detector.start(QThread.InheritPriority)
self.news_menu.set_custom_feeds(self.library_view.model().db.get_feeds())
def current_view(self):
'''Convenience method that returns the currently visible view '''
@ -538,6 +541,13 @@ class Main(MainWindow, Ui_MainWindow):
############################### Fetch news #################################
def customize_feeds(self, *args):
d = UserProfiles(self, self.library_view.model().db.get_feeds())
if d.exec_() == QDialog.Accepted:
feeds = tuple(d.profiles())
self.library_view.model().db.set_feeds(feeds)
self.news_menu.set_custom_feeds(feeds)
def fetch_news(self, data):
pt = PersistentTemporaryFile(suffix='.lrf')
pt.close()

View File

@ -17,6 +17,7 @@ from PyQt4.QtGui import QMenu, QIcon, QDialog, QAction
from libprs500.gui2.dialogs.password import PasswordDialog
from libprs500.ebooks.lrf.web import builtin_profiles, available_profiles
from libprs500.gui2.dialogs.user_profiles import UserProfiles
class NewsAction(QAction):
@ -39,13 +40,23 @@ class NewsAction(QAction):
class NewsMenu(QMenu):
def __init__(self):
def __init__(self, customize_feeds_func):
QMenu.__init__(self)
self.cac = QAction(QIcon(':/images/user_profile.svg'), _('Add a custom news source'), self)
self.connect(self.cac, SIGNAL('triggered(bool)'), customize_feeds_func)
self.addAction(self.cac)
self.custom_menu = CustomNewsMenu()
self.addMenu(self.custom_menu)
self.connect(self.custom_menu, SIGNAL('start_news_fetch(PyQt_PyObject, PyQt_PyObject)'),
self.fetch_news)
self.addSeparator()
for profile, module in zip(builtin_profiles, available_profiles):
self.addAction(NewsAction(profile, module, self))
def fetch_news(self, profile, module):
def fetch_news(self, profile, module=None):
if module is None:
module = profile.title
username = password = None
fetch = True
if profile.needs_subscription:
@ -57,7 +68,36 @@ class NewsMenu(QMenu):
else:
fetch = False
if fetch:
data = dict(profile=module, title=profile.title, username=username, password=password)
data = dict(profile=profile, title=profile.title, username=username, password=password)
self.emit(SIGNAL('fetch_news(PyQt_PyObject)'), data)
def set_custom_feeds(self, feeds):
self.custom_menu.set_feeds(feeds)
class CustomNewMenuItem(QAction):
def __init__(self, title, script, parent):
QAction.__init__(self, QIcon(':/images/user_profile.svg'), title, parent)
self.title = title
self.script = script
class CustomNewsMenu(QMenu):
def __init__(self):
QMenu.__init__(self)
self.setTitle(_('Custom news sources'))
self.connect(self, SIGNAL('triggered(QAction*)'), self.launch)
def launch(self, action):
profile = UserProfiles.create_class(action.script)
self.emit(SIGNAL('start_news_fetch(PyQt_PyObject, PyQt_PyObject)'),
profile, None)
def set_feeds(self, feeds):
self.clear()
for title, src in feeds:
self.addAction(CustomNewMenuItem(title, src, self))

View File

@ -12,10 +12,11 @@
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from libprs500.gui2 import qstring_to_unicode
'''
Miscellanous widgets used in the GUI
'''
from PyQt4.QtGui import QListView, QIcon, QFont, QLabel
from PyQt4.QtGui import QListView, QIcon, QFont, QLabel, QListWidget, QListWidgetItem
from PyQt4.QtCore import QAbstractListModel, QVariant, Qt, QSize, SIGNAL, QObject
from libprs500.gui2 import human_readable, NONE, TableView
@ -133,4 +134,35 @@ class FontFamilyModel(QAbstractListModel):
return self.families.index(family.strip())
class BasicListItem(QListWidgetItem):
def __init__(self, text, user_data=None):
QListWidgetItem.__init__(self, text)
self.user_data = user_data
def __eq__(self, other):
if hasattr(other, 'text'):
return self.text() == other.text()
return False
class BasicList(QListWidget):
def add_item(self, text, user_data=None, replace=False):
item = BasicListItem(text, user_data)
for oitem in self.items():
if oitem == item:
if replace:
self.takeItem(self.row(oitem))
else:
raise ValueError('Item already in list')
self.addItem(item)
def remove_selected_items(self, *args):
for item in self.selectedItems():
self.takeItem(self.row(item))
def items(self):
for i in range(self.count()):
yield self.item(i)

View File

@ -740,6 +740,16 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
conn.execute('pragma user_version=6')
conn.commit()
@staticmethod
def upgrade_version6(conn):
conn.executescript('''CREATE TABLE feeds ( id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
script TEXT NOT NULL,
UNIQUE(title)
);''')
conn.execute('pragma user_version=7')
conn.commit()
def __del__(self):
global _lock_file
import os
@ -765,6 +775,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
LibraryDatabase.upgrade_version4(self.conn)
if self.user_version == 5: # Upgrade to 6
LibraryDatabase.upgrade_version5(self.conn)
if self.user_version == 6: # Upgrade to 7
LibraryDatabase.upgrade_version6(self.conn)
def close(self):
global _lock_file
@ -1222,6 +1234,18 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE;
if data[i][0] == id:
return i
def get_feeds(self):
feeds = self.conn.execute('SELECT title, script FROM feeds').fetchall()
for title, script in feeds:
yield title, script
def set_feeds(self, feeds):
self.conn.execute('DELETE FROM feeds')
for title, script in feeds:
self.conn.execute('INSERT INTO feeds(title, script) VALUES (?, ?)',
(title, script))
self.conn.commit()
def delete_book(self, id):
'''
Removes book from self.cache, self.data and underlying database.