products/CPSRSS

changeset 232:8b941a0115c5

#446: local RSS Channels and management via normal CPS UI
author Georges Racinet on purity.racinet.fr <georges@racinet.fr>
date Sun, 05 Dec 2010 01:42:27 +0100
parents 6c108954bee9
children b1836f263b12
files CHANGES RSSChannel.py RSSChannelContainer.py RSSTool.py interfaces.py tests/testRSS.py
diffstat 6 files changed, 165 insertions(+), 31 deletions(-) [+]
line diff
     1.1 --- a/CHANGES
     1.2 +++ b/CHANGES
     1.3 @@ -3,7 +3,7 @@
     1.4  -
     1.5  New features
     1.6  ~~~~~~~~~~~~
     1.7 --
     1.8 +- #446: local RSS Channels and management via normal CPS UI
     1.9  Bug fixes
    1.10  ~~~~~~~~~
    1.11  -
     2.1 --- a/RSSChannel.py
     2.2 +++ b/RSSChannel.py
     2.3 @@ -15,8 +15,6 @@
     2.4  # along with this program; if not, write to the Free Software
     2.5  # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
     2.6  # 02111-1307, USA.
     2.7 -#
     2.8 -# $Id$
     2.9  """The RSS tool manages RSS channels and refreshes them.
    2.10  """
    2.11  
    2.12 @@ -180,11 +178,17 @@
    2.13      def _maybeRefresh(self):
    2.14          """Refresh if on lazy refresh and the delay has elapsed."""
    2.15  
    2.16 -        if not self.lazy_refresh: # acquired from parent (portal_rss)
    2.17 +        # GR: I find it doubtful that refresh lazyness and delay
    2.18 +        # are set at the tool, because they are likely to depend on the
    2.19 +        # rate of the feed itself. The tool should imo provide default values
    2.20 +        # keeping status quo for now.
    2.21 +        rss_tool = getToolByName(self, 'portal_rss')
    2.22 +        if not rss_tool.lazy_refresh:
    2.23              logger.debug('Not on lazy refresh')
    2.24              self._refresh()
    2.25              return
    2.26 -        delay = self.refresh_delay # acquired from parent (portal_rss)
    2.27 +
    2.28 +        delay = rss_tool.refresh_delay
    2.29          now = int(time.time())
    2.30          if now - self._refresh_time > delay:
    2.31              logger.debug('Refreshing %r', self.id)
    2.32 @@ -209,8 +213,12 @@
    2.33          if not url.startswith('http://') or url.startswith('https://'):
    2.34              data = {'channel': {}, 'items': []}
    2.35          try :
    2.36 -            if self.channel_proxy:
    2.37 -                proxy_handler = ProxyHandler({'http': self.channel_proxy})
    2.38 +            proxy = self.channel_proxy
    2.39 +            # GR TODO : replace this by a portal-wide setting and maybe use
    2.40 +            # system-wide setting (environ['http_proxy'])
    2.41 +            if proxy:
    2.42 +                logger.info("Using HTTP proxy %r", proxy)
    2.43 +                proxy_handler = ProxyHandler({'http': proxy})
    2.44                  handlers = [proxy_handler]
    2.45              else:
    2.46                  handlers = []
     3.1 new file mode 100644
     3.2 --- /dev/null
     3.3 +++ b/RSSChannelContainer.py
     3.4 @@ -0,0 +1,124 @@
     3.5 +# (C) Copyright 2003 Nuxeo SARL <http://nuxeo.com>
     3.6 +#               2010 Georges Racinet <georges@racinet.fr>
     3.7 +# Authors: Emmanuel Pietriga (ep@nuxeo.com)
     3.8 +#          Georges Racinet <georges@racinet.fr>
     3.9 +#
    3.10 +# This program is free software; you can redistribute it and/or modify
    3.11 +# it under the terms of the GNU General Public License version 2 as published
    3.12 +# by the Free Software Foundation.
    3.13 +#
    3.14 +# This program is distributed in the hope that it will be useful,
    3.15 +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    3.16 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    3.17 +# GNU General Public License for more details.
    3.18 +#
    3.19 +# You should have received a copy of the GNU General Public License
    3.20 +# along with this program; if not, write to the Free Software
    3.21 +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
    3.22 +# 02111-1307, USA.
    3.23 +#
    3.24 +"""The RSS tool manages RSS channels and refreshes them.
    3.25 +"""
    3.26 +
    3.27 +from zLOG import LOG, DEBUG
    3.28 +from Globals import InitializeClass, DTMLFile
    3.29 +from AccessControl import ClassSecurityInfo
    3.30 +from OFS.Folder import Folder
    3.31 +
    3.32 +from Products.CMFCore.permissions import View, ManagePortal
    3.33 +from Products.CMFCore.utils import UniqueObject
    3.34 +
    3.35 +from RSSChannel import addRSSChannel, RSSChannel_meta_type
    3.36 +
    3.37 +from zope.interface import implements
    3.38 +
    3.39 +from Products.CPSRSS.interfaces import IRSSTool
    3.40 +from Products.CPSRSS.interfaces import IRSSChannelContainer
    3.41 +
    3.42 +class RSSChannelContainer(Folder):
    3.43 +    """RSS tool, a container for RSS channels that can refresh them."""
    3.44 +
    3.45 +    implements(IRSSChannelContainer)
    3.46 +
    3.47 +    meta_type = 'RSS Channel Container'
    3.48 +
    3.49 +    security = ClassSecurityInfo()
    3.50 +    security.declareObjectProtected(View)
    3.51 +
    3.52 +    def __init__(self, zid):
    3.53 +        self._setId(zid)
    3.54 +
    3.55 +    #
    3.56 +    # API
    3.57 +    #
    3.58 +
    3.59 +    security.declareProtected(ManagePortal, 'refresh')
    3.60 +    def refresh(self, REQUEST=None):
    3.61 +        """Refresh all the channels from their source."""
    3.62 +        for ob in self.objectValues(RSSChannel_meta_type):
    3.63 +            ob.refresh()
    3.64 +        if REQUEST:
    3.65 +            REQUEST.RESPONSE.redirect(
    3.66 +                '%s/manage_main?manage_tabs_message=Refreshed.'
    3.67 +                % self.absolute_url())
    3.68 +
    3.69 +    #
    3.70 +    # CMF views
    3.71 +    #
    3.72 +    def __call__(self, REQUEST=None, **kw):
    3.73 +        """Default view."""
    3.74 +        return self.view(REQUEST=REQUEST, **kw)
    3.75 +
    3.76 +    index_html = None  # This special value informs ZPublisher to use __call__
    3.77 +
    3.78 +    security.declareProtected(View, 'view')
    3.79 +    def view(self, REQUEST=None, **kw):
    3.80 +        """Default view."""
    3.81 +        # FIXME rsstool_view doesn't exist
    3.82 +        return self.rsstool_view(REQUEST=REQUEST, **kw)
    3.83 +
    3.84 +    #
    3.85 +    # ZMI
    3.86 +    #
    3.87 +    _properties = (
    3.88 +        {'id': 'refresh_delay', 'type': 'int', 'mode': 'w', 
    3.89 +         'label': 'Refresh Delay'},
    3.90 +        {'id': 'lazy_refresh', 'type': 'boolean', 'mode': 'w', 
    3.91 +         'label': 'Lazy Refresh'},
    3.92 +    )
    3.93 +
    3.94 +    title = ''
    3.95 +    refresh_delay = 1200 # 20 minutes
    3.96 +    lazy_refresh = 1
    3.97 +
    3.98 +    all_meta_types = (
    3.99 +        {'name': RSSChannel_meta_type,
   3.100 +         'action': 'manage_addRSSChannelForm',
   3.101 +         'permission': ManagePortal,
   3.102 +         },
   3.103 +        )
   3.104 +
   3.105 +    manage_addRSSChannelForm = DTMLFile('zmi/addRSSChannelForm', globals())
   3.106 +
   3.107 +    security.declareProtected(ManagePortal, 'manage_addRSSChannel')
   3.108 +    def manage_addRSSChannel(self, id, channel_url, REQUEST=None, **kw):
   3.109 +        """Add a RSS Channel from the ZMI."""
   3.110 +        if REQUEST:
   3.111 +            kw.update(REQUEST.form)
   3.112 +            del kw['id']
   3.113 +        container = self
   3.114 +        addRSSChannel(container, id, channel_url)
   3.115 +        if REQUEST:
   3.116 +            REQUEST.RESPONSE.redirect('%s/%s/manage_workspace'
   3.117 +                                      % (container.absolute_url(), id))
   3.118 +
   3.119 +    manage_options = (Folder.manage_options[:1] + # contents
   3.120 +                      ({'label': 'Refresh', 'action': 'refreshForm'},) +
   3.121 +                      Folder.manage_options[1:])
   3.122 +
   3.123 +    refreshForm = DTMLFile('zmi/refreshForm', globals())
   3.124 +
   3.125 +
   3.126 +InitializeClass(RSSChannelContainer)
   3.127 +
   3.128 +
     4.1 --- a/RSSTool.py
     4.2 +++ b/RSSTool.py
     4.3 @@ -22,11 +22,11 @@
     4.4  import logging
     4.5  from Globals import InitializeClass, DTMLFile
     4.6  from AccessControl import ClassSecurityInfo
     4.7 -from OFS.Folder import Folder
     4.8  
     4.9  from Products.CMFCore.permissions import View, ManagePortal
    4.10  from Products.CMFCore.utils import UniqueObject
    4.11  
    4.12 +from RSSChannelContainer import RSSChannelContainer
    4.13  from RSSChannel import addRSSChannel, RSSChannel_meta_type
    4.14  
    4.15  from zope.interface import implements
    4.16 @@ -35,7 +35,8 @@
    4.17  
    4.18  logger = logging.getLogger(__name__)
    4.19  
    4.20 -class RSSTool(UniqueObject, Folder):
    4.21 +class RSSTool(UniqueObject, RSSChannelContainer):
    4.22 +
    4.23      """RSS tool, a container for RSS channels that can refresh them."""
    4.24  
    4.25      implements(IRSSTool)
    4.26 @@ -46,18 +47,8 @@
    4.27      security = ClassSecurityInfo()
    4.28      security.declareObjectProtected(View)
    4.29  
    4.30 -    #
    4.31 -    # API
    4.32 -    #
    4.33 -    security.declareProtected(ManagePortal, 'refresh')
    4.34 -    def refresh(self, REQUEST=None):
    4.35 -        """Refresh all the channels from their source."""
    4.36 -        for ob in self.objectValues(RSSChannel_meta_type):
    4.37 -            ob.refresh()
    4.38 -        if REQUEST:
    4.39 -            REQUEST.RESPONSE.redirect(
    4.40 -                '%s/manage_main?manage_tabs_message=Refreshed.'
    4.41 -                % self.absolute_url())
    4.42 +    def __init__(self):
    4.43 +        super(self.__class__, self).__init__(self.id)
    4.44  
    4.45      #
    4.46      # CMF views
    4.47 @@ -110,11 +101,4 @@
    4.48              REQUEST.RESPONSE.redirect('%s/%s/manage_workspace'
    4.49                                        % (container.absolute_url(), id))
    4.50  
    4.51 -    manage_options = (Folder.manage_options[:1] + # contents
    4.52 -                      ({'label': 'Refresh', 'action': 'refreshForm'},) +
    4.53 -                      Folder.manage_options[1:])
    4.54 -
    4.55 -    refreshForm = DTMLFile('zmi/refreshForm', globals())
    4.56 -
    4.57 -
    4.58  InitializeClass(RSSTool)
     5.1 --- a/interfaces.py
     5.2 +++ b/interfaces.py
     5.3 @@ -23,7 +23,13 @@
     5.4  from zope.interface import Interface
     5.5  
     5.6  
     5.7 -class IRSSTool(Interface):
     5.8 +class IRSSChannelContainer(Interface):
     5.9 +    """RSS Channel Container.
    5.10 +
    5.11 +    Placeful container to put RSS channels in
    5.12 +    """
    5.13 +
    5.14 +class IRSSTool(IRSSChannelContainer):
    5.15      """RSS Tool.
    5.16      """
    5.17  
     6.1 --- a/tests/testRSS.py
     6.2 +++ b/tests/testRSS.py
     6.3 @@ -2,6 +2,7 @@
     6.4  
     6.5  import unittest, os.path
     6.6  
     6.7 +from OFS.Folder import Folder
     6.8  from CPSRSSTestCase import CPSRSSTestCase
     6.9  from CPSRSSTestCase import ZopeRSSTestCase
    6.10  
    6.11 @@ -13,7 +14,12 @@
    6.12  
    6.13  class TestRSS(object):
    6.14  
    6.15 +    def localContainer(self):
    6.16 +        """Depends on subclass."""
    6.17 +        raise NotImplementedError
    6.18 +
    6.19      def _localContainer(self, folder):
    6.20 +        """Common code called by subclass."""
    6.21          folder._setObject('.cps_rss', RSSChannelContainer('.cps_rss'))
    6.22          return folder['.cps_rss']
    6.23  
    6.24 @@ -54,11 +60,17 @@
    6.25      def testChannelNotLazy(self):
    6.26          self._testChannel(lazy_refresh=0)
    6.27  
    6.28 +    def testLocalChannelLazy(self):
    6.29 +        self._testLocalChannel(lazy_refresh=1)
    6.30 +
    6.31 +    def testLocalChannelNotLazy(self):
    6.32 +        self._testLocalChannel(lazy_refresh=0)
    6.33 +
    6.34  class CPSTestRSS(TestRSS, CPSRSSTestCase):
    6.35      """Run the tests in a full CPS portal context."""
    6.36  
    6.37      def localContainer(self):
    6.38 -        return _localContainer(self.portal.workspaces)
    6.39 +        return self._localContainer(self.portal.workspaces)
    6.40  
    6.41  
    6.42  class ZopeTestRSS(TestRSS, ZopeRSSTestCase):
    6.43 @@ -66,7 +78,7 @@
    6.44  
    6.45      def localContainer(self):
    6.46          self.folder._setObject('subfold', Folder('subfold'))
    6.47 -        return _localContainer(self.folder.subfold)
    6.48 +        return self._localContainer(self.folder.subfold)
    6.49  
    6.50  def test_suite():
    6.51      suites = [unittest.makeSuite(cls) for cls in (ZopeTestRSS, CPSTestRSS)]