From 2fa7063034e67bc7a6d83f67c74f1474acde165d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 24 Jan 2008 00:10:19 +0000 Subject: [PATCH] Add support for adding custom news profiles to the GUI --- src/libprs500/ebooks/lrf/web/convert_from.py | 18 +- .../ebooks/lrf/web/profiles/__init__.py | 36 +- .../ebooks/lrf/web/profiles/dilbert.py | 2 - src/libprs500/gui2/dialogs/user_profiles.py | 187 + src/libprs500/gui2/dialogs/user_profiles.ui | 382 ++ src/libprs500/gui2/images.qrc | 1 + .../gui2/images/dialog_information.svg | 4 +- src/libprs500/gui2/images/user_profile.svg | 4750 +++++++++++++++++ src/libprs500/gui2/main.py | 12 +- src/libprs500/gui2/news.py | 48 +- src/libprs500/gui2/widgets.py | 36 +- src/libprs500/library/database.py | 24 + 12 files changed, 5472 insertions(+), 28 deletions(-) create mode 100644 src/libprs500/gui2/dialogs/user_profiles.py create mode 100644 src/libprs500/gui2/dialogs/user_profiles.ui create mode 100644 src/libprs500/gui2/images/user_profile.svg diff --git a/src/libprs500/ebooks/lrf/web/convert_from.py b/src/libprs500/ebooks/lrf/web/convert_from.py index 38939e8ade..9a42945f9e 100644 --- a/src/libprs500/ebooks/lrf/web/convert_from.py +++ b/src/libprs500/ebooks/lrf/web/convert_from.py @@ -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 diff --git a/src/libprs500/ebooks/lrf/web/profiles/__init__.py b/src/libprs500/ebooks/lrf/web/profiles/__init__.py index e815dbb88f..66d8d2a7c2 100644 --- a/src/libprs500/ebooks/lrf/web/profiles/__init__.py +++ b/src/libprs500/ebooks/lrf/web/profiles/__init__.py @@ -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 @@ -52,14 +53,17 @@ class DefaultProfile(object): preprocess_regexps = [] # 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): @@ -134,8 +138,8 @@ class DefaultProfile(object): prefix = 'file:' if iswindows else '' clist += u'
  • %s
  • \n'%(prefix+cfile, category) src = build_sub_index(category, articles[category]) - open(cfile, 'wb').write(src.encode('utf-8')) - + open(cfile, 'wb').write(src.encode('utf-8')) + src = '''\ @@ -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'
  • %(title)s [%(date)s]
    \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]+'…' + item['description'] = cutoff(item['description'], self.summary_length)+'…' self.article_counter = self.article_counter + 1 url = os.path.join(self.temp_dir, 'article%d.html'%self.article_counter) item['url'] = url @@ -383,7 +383,8 @@ class FullContentProfile(DefaultProfile): prefix = 'file:' if iswindows else '' clist += u'
  • %s
  • \n'%(prefix+cfile, category) src = build_sub_index(category, articles[category]) - open(cfile, 'wb').write(src.encode('utf-8')) + open(cfile, 'wb').write(src.encode('utf-8')) + open('/tmp/category'+str(cnum)+'.html', 'wb').write(src.encode('utf-8')) src = '''\ @@ -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] \ No newline at end of file diff --git a/src/libprs500/ebooks/lrf/web/profiles/dilbert.py b/src/libprs500/ebooks/lrf/web/profiles/dilbert.py index 56025205bd..927fb4ef6f 100644 --- a/src/libprs500/ebooks/lrf/web/profiles/dilbert.py +++ b/src/libprs500/ebooks/lrf/web/profiles/dilbert.py @@ -23,8 +23,6 @@ Fetch Dilbert. from libprs500.ebooks.lrf.web.profiles import DefaultProfile -import re - class Dilbert(DefaultProfile): title = 'Dilbert' diff --git a/src/libprs500/gui2/dialogs/user_profiles.py b/src/libprs500/gui2/dialogs/user_profiles.py new file mode 100644 index 0000000000..245583fff2 --- /dev/null +++ b/src/libprs500/gui2/dialogs/user_profiles.py @@ -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', + '

    Could not create profile. Error:
    %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', + '

    Could not create profile. Error:
    %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 + + \ No newline at end of file diff --git a/src/libprs500/gui2/dialogs/user_profiles.ui b/src/libprs500/gui2/dialogs/user_profiles.ui new file mode 100644 index 0000000000..d08d8284b2 --- /dev/null +++ b/src/libprs500/gui2/dialogs/user_profiles.ui @@ -0,0 +1,382 @@ + + Dialog + + + + 0 + 0 + 703 + 661 + + + + Add custom RSS feed + + + :/images/user_profile.svg + + + + + + Available user profiles + + + + + + QAbstractItemView::MultiSelection + + + + + + + Add/Update &profile + + + :/images/plus.svg + + + + + + + &Remove profile + + + :/images/list_remove.svg + + + + + + + + + + + + Switch to Advanced mode + + + + + + + 0 + + + + + + + <html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'DejaVu Sans'; font-size:10pt; font-weight:400; font-style:normal;"> +<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. <br />For most feeds, you will have to use the "Advanced" setting to further customize the fetch process.<br />The Basic tab is useful mainly for feeds that have the full article content embedded within them.</p></body></html> + + + Qt::RichText + + + true + + + + + + + + + Profile &title: + + + profile_title + + + + + + + + 75 + true + + + + + + + + &Summary length: + + + summary_length + + + + + + + characters + + + 0 + + + 100000 + + + 500 + + + + + + + &Oldest article: + + + oldest_article + + + + + + + The oldest article to download + + + days + + + 1 + + + 365 + + + 7 + + + + + + + &Max. number of articles per feed: + + + max_articles + + + + + + + Maximum number of articles to download per feed. + + + 5 + + + 100 + + + 10 + + + + + + + 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. + + + Try to download &full articles + + + + + + + + + Feeds in profile + + + + + + QAbstractItemView::MultiSelection + + + + + + + Remove feed from profile + + + ... + + + :/images/list_remove.svg + + + + + + + + + + Add feed to profile + + + + + + &Feed title: + + + feed_title + + + + + + + + + + Feed &URL: + + + feed_url + + + + + + + + + + Add feed to profile + + + &Add feed + + + :/images/plus.svg + + + + + + + + + + + + + + For help with writing advanced news profiles, please visit <a href="https://libprs500.kovidgoyal.net/wiki/UserProfiles">UserProfiles</a> + + + true + + + true + + + + + + + Profile source code (python) + + + + + + + DejaVu Sans Mono + + + + QTextEdit::NoWrap + + + false + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + + + + + + + BasicList + QListWidget +

    widgets.h
    + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 446 + 649 + + + 0 + 632 + + + + + buttonBox + rejected() + Dialog + reject() + + + 175 + 643 + + + 176 + 636 + + + + + diff --git a/src/libprs500/gui2/images.qrc b/src/libprs500/gui2/images.qrc index e042745ca6..e81d7ed9ea 100644 --- a/src/libprs500/gui2/images.qrc +++ b/src/libprs500/gui2/images.qrc @@ -3,6 +3,7 @@ images/back.svg images/book.svg images/search.svg + images/user_profile.svg images/chapters.svg images/clear_left.svg images/config.svg diff --git a/src/libprs500/gui2/images/dialog_information.svg b/src/libprs500/gui2/images/dialog_information.svg index 42c1247bb6..b43e46c5e9 100644 --- a/src/libprs500/gui2/images/dialog_information.svg +++ b/src/libprs500/gui2/images/dialog_information.svg @@ -2902,7 +2902,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libprs500/gui2/main.py b/src/libprs500/gui2/main.py index 1b21a62bb8..1c41b4e62e 100644 --- a/src/libprs500/gui2/main.py +++ b/src/libprs500/gui2/main.py @@ -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() diff --git a/src/libprs500/gui2/news.py b/src/libprs500/gui2/news.py index 37b53fd6d9..7a59c7f198 100644 --- a/src/libprs500/gui2/news.py +++ b/src/libprs500/gui2/news.py @@ -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)) + \ No newline at end of file diff --git a/src/libprs500/gui2/widgets.py b/src/libprs500/gui2/widgets.py index 136aa22d99..000efe1c6c 100644 --- a/src/libprs500/gui2/widgets.py +++ b/src/libprs500/gui2/widgets.py @@ -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 @@ -132,5 +133,36 @@ class FontFamilyModel(QAbstractListModel): def index_of(self, family): 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) diff --git a/src/libprs500/library/database.py b/src/libprs500/library/database.py index ed5716e05c..dcb7bcdef3 100644 --- a/src/libprs500/library/database.py +++ b/src/libprs500/library/database.py @@ -739,6 +739,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 @@ -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.