Browse files

Move new trunk into place.

svn path=/feedfeeder/trunk/; revision=65293
  • Loading branch information...
0 parents commit 3714d0fb11a5511ddb502b230dcb573811cba963 @mauritsvanrees mauritsvanrees committed May 20, 2008
Showing with 6,525 additions and 0 deletions.
  1. +6 −0 Products/__init__.py
  2. +23 −0 Products/feedfeeder/Extensions/AppInstall.py
  3. +214 −0 Products/feedfeeder/Extensions/Install.py
  4. +1 −0 Products/feedfeeder/Extensions/__init__.py
  5. +26 −0 Products/feedfeeder/HISTORY.txt
  6. +91 −0 Products/feedfeeder/README.txt
  7. +85 −0 Products/feedfeeder/__init__.py
  8. 0 Products/feedfeeder/browser/__init__.py
  9. +46 −0 Products/feedfeeder/browser/configure.zcml
  10. +48 −0 Products/feedfeeder/browser/feed-folder.pt
  11. +115 −0 Products/feedfeeder/browser/feed-item.pt
  12. +95 −0 Products/feedfeeder/browser/feed.py
  13. +58 −0 Products/feedfeeder/config.py
  14. +31 −0 Products/feedfeeder/configure.zcml
  15. +16 −0 Products/feedfeeder/content/__init__.py
  16. +159 −0 Products/feedfeeder/content/folder.py
  17. +164 −0 Products/feedfeeder/content/item.py
  18. +65 −0 Products/feedfeeder/contenthandler.py
  19. +1 −0 Products/feedfeeder/doc/__init__.py
  20. +166 −0 Products/feedfeeder/doc/feedconsuming.txt
  21. +291 −0 Products/feedfeeder/doc/feedfeeder-integration.txt
  22. +16 −0 Products/feedfeeder/events.py
  23. +2,864 −0 Products/feedfeeder/feedparser.py
  24. +43 −0 Products/feedfeeder/i18n/feedfeeder-de.po
  25. +43 −0 Products/feedfeeder/i18n/feedfeeder-eu.po
  26. +53 −0 Products/feedfeeder/i18n/feedfeeder-fr.po
  27. +43 −0 Products/feedfeeder/i18n/feedfeeder-nl.po
  28. +21 −0 Products/feedfeeder/i18n/feedfeeder-plone-de.po
  29. +22 −0 Products/feedfeeder/i18n/feedfeeder-plone-eu.po
  30. +25 −0 Products/feedfeeder/i18n/feedfeeder-plone-fr.po
  31. +21 −0 Products/feedfeeder/i18n/feedfeeder-plone-nl.po
  32. +21 −0 Products/feedfeeder/i18n/feedfeeder-plone.pot
  33. +52 −0 Products/feedfeeder/i18n/feedfeeder.pot
  34. +38 −0 Products/feedfeeder/i18n/generated.pot
  35. +19 −0 Products/feedfeeder/interfaces/__init__.py
  36. +31 −0 Products/feedfeeder/interfaces/consumer.py
  37. +52 −0 Products/feedfeeder/interfaces/container.py
  38. +31 −0 Products/feedfeeder/interfaces/contenthandler.py
  39. +31 −0 Products/feedfeeder/interfaces/folderview.py
  40. +22 −0 Products/feedfeeder/interfaces/item.py
  41. +16 −0 Products/feedfeeder/profiles/default/actions.xml
  42. +9 −0 Products/feedfeeder/profiles/default/catalog.xml
  43. +7 −0 Products/feedfeeder/profiles/default/factorytool.xml
  44. +1 −0 Products/feedfeeder/profiles/default/feedfeeder_various.txt
  45. +9 −0 Products/feedfeeder/profiles/default/import_steps.xml
  46. +8 −0 Products/feedfeeder/profiles/default/propertiestool.xml
  47. +11 −0 Products/feedfeeder/profiles/default/skins.xml
  48. +7 −0 Products/feedfeeder/profiles/default/types.xml
  49. +55 −0 Products/feedfeeder/profiles/default/types/FeedFeederItem.xml
  50. +60 −0 Products/feedfeeder/profiles/default/types/FeedfeederFolder.xml
  51. +14 −0 Products/feedfeeder/rebuild_i18n.sh
  52. +34 −0 Products/feedfeeder/setuphandlers.py
  53. BIN Products/feedfeeder/skins/feedfeeder/feed_icon.gif
  54. +12 −0 Products/feedfeeder/skins/feedfeeder/feed_viewlet.pt
  55. +21 −0 Products/feedfeeder/skins/feedfeeder/latest_items.pt
  56. +8 −0 Products/feedfeeder/skins/feedfeeder/readme.txt
  57. +96 −0 Products/feedfeeder/tests/MainTestCase.py
  58. +14 −0 Products/feedfeeder/tests/__init__.py
  59. +107 −0 Products/feedfeeder/tests/framework.py
  60. +29 −0 Products/feedfeeder/tests/runalltests.py
  61. +61 −0 Products/feedfeeder/tests/samples/examplefeed.xml
  62. +53 −0 Products/feedfeeder/tests/samples/samplefeed1.xml
  63. +29 −0 Products/feedfeeder/tests/samples/samplefeed2.xml
  64. +19 −0 Products/feedfeeder/tests/samples/samplefeed3.xml
  65. +19 −0 Products/feedfeeder/tests/samples/samplefeed4.xml
  66. BIN Products/feedfeeder/tests/samples/test1.pdf
  67. +1 −0 Products/feedfeeder/tests/samples/test2.doc
  68. +1 −0 Products/feedfeeder/tests/samples/test3.xls
  69. +30 −0 Products/feedfeeder/tests/testDocIntegrationTests.py
  70. +44 −0 Products/feedfeeder/tests/testDocUnitTests.py
  71. +62 −0 Products/feedfeeder/tests/testFeedfeederFolder.py
  72. +230 −0 Products/feedfeeder/utilities.py
  73. +1 −0 Products/feedfeeder/version.txt
  74. +35 −0 docs/INSTALL.txt
  75. +222 −0 docs/LICENSE.GPL
  76. +16 −0 docs/LICENSE.txt
  77. +35 −0 setup.py
6 Products/__init__.py
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+ __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+ from pkgutil import extend_path
+ __path__ = extend_path(__path__, __name__)
23 Products/feedfeeder/Extensions/AppInstall.py
@@ -0,0 +1,23 @@
+from StringIO import StringIO
+from Products.CMFCore.utils import getToolByName
+
+
+def install(site):
+ out = StringIO()
+ applyGenericSetupProfile(site, out)
+
+
+def applyGenericSetupProfile(site, out):
+ """Just apply our own extension profile.
+ """
+
+ setup_tool = getToolByName(site, 'portal_setup')
+ setup_tool.setImportContext('profile-feedfeeder:default')
+ print >> out, "Applying the generic setup profile for feedfeeder..."
+ setup_tool.runAllImportSteps(purge_old=False)
+ try:
+ setup_tool.setImportContext('profile-CMFPlone:plone')
+ except KeyError:
+ # Plone 3.0 has a different profile name
+ setup_tool.setImportContext('profile-Products.CMFPlone:plone')
+ print >> out, "Applied the generic setup profile for feedfeeder"
214 Products/feedfeeder/Extensions/Install.py
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+
+import os.path
+import sys
+from StringIO import StringIO
+from sets import Set
+from App.Common import package_home
+from Products.CMFCore.utils import getToolByName
+from Products.CMFCore.utils import manage_addTool
+from Products.ExternalMethod.ExternalMethod import ExternalMethod
+from zExceptions import NotFound, BadRequest
+
+from Products.Archetypes.Extensions.utils import installTypes
+from Products.Archetypes.Extensions.utils import install_subskin
+from Products.Archetypes.config import TOOL_NAME as ARCHETYPETOOLNAME
+from Products.Archetypes.atapi import listTypes
+from Products.feedfeeder.config import PROJECTNAME
+from Products.feedfeeder.config import product_globals as GLOBALS
+
+def install(self, reinstall=False):
+ """ External Method to install feedfeeder """
+ out = StringIO()
+ print >> out, "Installation log of %s:" % PROJECTNAME
+
+ # If the config contains a list of dependencies, try to install
+ # them. Add a list called DEPENDENCIES to your custom
+ # AppConfig.py (imported by config.py) to use it.
+ try:
+ from Products.feedfeeder.config import DEPENDENCIES
+ except:
+ DEPENDENCIES = []
+ portal = getToolByName(self,'portal_url').getPortalObject()
+ quickinstaller = portal.portal_quickinstaller
+ for dependency in DEPENDENCIES:
+ print >> out, "Installing dependency %s:" % dependency
+ quickinstaller.installProduct(dependency)
+ import transaction
+ transaction.savepoint(optimistic=True)
+
+ classes = listTypes(PROJECTNAME)
+ installTypes(self, out,
+ classes,
+ PROJECTNAME)
+ install_subskin(self, out, GLOBALS)
+
+
+ # try to call a workflow install method
+ # in 'InstallWorkflows.py' method 'installWorkflows'
+ try:
+ installWorkflows = ExternalMethod('temp', 'temp',
+ PROJECTNAME+'.InstallWorkflows',
+ 'installWorkflows').__of__(self)
+ except NotFound:
+ installWorkflows = None
+
+ if installWorkflows:
+ print >>out,'Workflow Install:'
+ res = installWorkflows(self,out)
+ print >>out,res or 'no output'
+ else:
+ print >>out,'no workflow install'
+
+
+ # enable portal_factory for given types
+ factory_tool = getToolByName(self,'portal_factory')
+ factory_types=[
+ "FeedConsumer",
+ "StandardContentHandler",
+ "FeedfeederFolder",
+ "FeedFeederItem",
+ ] + factory_tool.getFactoryTypes().keys()
+ factory_tool.manage_setPortalFactoryTypes(listOfTypeIds=factory_types)
+
+ from Products.feedfeeder.config import STYLESHEETS
+ try:
+ portal_css = getToolByName(portal, 'portal_css')
+ for stylesheet in STYLESHEETS:
+ try:
+ portal_css.unregisterResource(stylesheet['id'])
+ except:
+ pass
+ defaults = {'id': '',
+ 'media': 'all',
+ 'enabled': True}
+ defaults.update(stylesheet)
+ portal_css.manage_addStylesheet(**defaults)
+ except:
+ # No portal_css registry
+ pass
+ from Products.feedfeeder.config import JAVASCRIPTS
+ try:
+ portal_javascripts = getToolByName(portal, 'portal_javascripts')
+ for javascript in JAVASCRIPTS:
+ try:
+ portal_javascripts.unregisterResource(javascript['id'])
+ except:
+ pass
+ defaults = {'id': ''}
+ defaults.update(javascript)
+ portal_javascripts.registerScript(**defaults)
+ except:
+ # No portal_javascripts registry
+ pass
+
+ # try to call a custom install method
+ # in 'AppInstall.py' method 'install'
+ try:
+ install = ExternalMethod('temp', 'temp',
+ PROJECTNAME+'.AppInstall', 'install')
+ except NotFound:
+ install = None
+
+ if install:
+ print >>out,'Custom Install:'
+ try:
+ res = install(self, reinstall)
+ except TypeError:
+ res = install(self)
+ if res:
+ print >>out,res
+ else:
+ print >>out,'no output'
+ else:
+ print >>out,'no custom install'
+ return out.getvalue()
+
+def uninstall(self, reinstall=False):
+ out = StringIO()
+
+ # try to call a workflow uninstall method
+ # in 'InstallWorkflows.py' method 'uninstallWorkflows'
+ try:
+ uninstallWorkflows = ExternalMethod('temp', 'temp',
+ PROJECTNAME+'.InstallWorkflows',
+ 'uninstallWorkflows').__of__(self)
+ except NotFound:
+ uninstallWorkflows = None
+
+ if uninstallWorkflows:
+ print >>out, 'Workflow Uninstall:'
+ res = uninstallWorkflows(self, out)
+ print >>out, res or 'no output'
+ else:
+ print >>out,'no workflow uninstall'
+
+ # try to call a custom uninstall method
+ # in 'AppInstall.py' method 'uninstall'
+ try:
+ uninstall = ExternalMethod('temp', 'temp',
+ PROJECTNAME+'.AppInstall', 'uninstall')
+ except:
+ uninstall = None
+
+ if uninstall:
+ print >>out,'Custom Uninstall:'
+ try:
+ res = uninstall(self, reinstall)
+ except TypeError:
+ res = uninstall(self)
+ if res:
+ print >>out,res
+ else:
+ print >>out,'no output'
+ else:
+ print >>out,'no custom uninstall'
+
+ return out.getvalue()
+
+def beforeUninstall(self, reinstall, product, cascade):
+ """ try to call a custom beforeUninstall method in 'AppInstall.py'
+ method 'beforeUninstall'
+ """
+ out = StringIO()
+ try:
+ beforeuninstall = ExternalMethod('temp', 'temp',
+ PROJECTNAME+'.AppInstall', 'beforeUninstall')
+ except:
+ beforeuninstall = []
+
+ if beforeuninstall:
+ print >>out, 'Custom beforeUninstall:'
+ res = beforeuninstall(self, reinstall=reinstall
+ , product=product
+ , cascade=cascade)
+ if res:
+ print >>out, res
+ else:
+ print >>out, 'no output'
+ else:
+ print >>out, 'no custom beforeUninstall'
+ return (out,cascade)
+
+def afterInstall(self, reinstall, product):
+ """ try to call a custom afterInstall method in 'AppInstall.py' method
+ 'afterInstall'
+ """
+ out = StringIO()
+ try:
+ afterinstall = ExternalMethod('temp', 'temp',
+ PROJECTNAME+'.AppInstall', 'afterInstall')
+ except:
+ afterinstall = None
+
+ if afterinstall:
+ print >>out, 'Custom afterInstall:'
+ res = afterinstall(self, product=None
+ , reinstall=None)
+ if res:
+ print >>out, res
+ else:
+ print >>out, 'no output'
+ else:
+ print >>out, 'no custom afterInstall'
+ return out
1 Products/feedfeeder/Extensions/__init__.py
@@ -0,0 +1 @@
+# make me a python module
26 Products/feedfeeder/HISTORY.txt
@@ -0,0 +1,26 @@
+1.0 beta 4 (unreleased)
+-----------------------
+
+- Eggification: you can now install it as the Products.feedfeeder
+ egg. [maurits]
+
+
+1.0 beta 3 (13 May 2008)
+------------------------
+
+- In the tests, use plone_workflow explicitly, so it is easier to test
+ on both Plone 2.5 and 3.0. [maurits]
+
+- Make update_feed_items available in the object_buttons for Plone 3,
+ using new small @@is_feedcontainer as condition. [maurits]
+
+- Avoid deprecation warnings for events and interfaces. [maurits]
+
+- Remove semicolon in page template that broke in Plone 3. [maurits]
+
+- Fix imports so they work in Plone 3 as well, without deprecation
+ warnings. [derstappenit]
+
+
+1.0 beta 2 (2 January 2008)
+---------------------------
91 Products/feedfeeder/README.txt
@@ -0,0 +1,91 @@
+Feedfeeder
+==========
+
+Feedfeeder has just a few things it needs to do:
+
+- Read in a few ATOM feeds (not too many).
+
+- Create FeedfeederItems out of the entries pulled from the ATOM feeds.
+ Any feed items that contain enclosures will have the enclosures
+ pulled down and added as File items to the feed item.
+
+- This means figuring out which items are new, which also means having
+ a good ID generating mechanism.
+
+
+Wait, no existing product?
+--------------------------
+
+There's a whole slew of RSS/ATOM reading products for zope and
+plone. None of them seemed to be a good fit. There was only one
+product that actually stored the entries in the zope database, but
+that was aimed at a lot of users individually adding a lot of feeds,
+so it needed either a separate ZEO process (old version) or a
+standalone mysql database (new version).
+
+All the other products didn't store the entries in the database, were
+old/unmaintained/etc.
+
+In a sense, we're using an existing product as we use Mark Pilgrim's
+excellent feedparser (http://feedparser.org) that'll do the actual
+ATOM reading for us.
+
+
+Product name
+------------
+
+The product feeds the content of ATOM feeds to plone as document/file
+content types. So "feedfeeder" sort of suggested itself as a funny
+name. Fun is important :-)
+
+
+Product structure
+-----------------
+
+I'm using archgenxml to generate the boiler plate stuff. There's a
+'generate.sh' shell script that'll call archgenxml for you. Nothing
+fancy.
+
+The feedfeeder's content types are:
+ - folder.FeedfeederFolder
+ - item.FeedfeederItem
+
+
+How it works
+------------
+
+A feedfeeder is a folder which contains all the previously-added feed
+entries as documents or files. It has a 'feeds' attribute that
+contains a list of feeds to read.
+
+Feedparser is called periodically (through a cron job?) to parse the
+feeds. The UID of the items in the feed are converted to a suitable
+filename (md5 hex hash of the atom id of the entry), that way you can
+detect whether there are new items.
+
+New items are turned into feed items.
+
+Scheduled updates for feed folders
+
+Zope can be configured to periodically trigger a url call.
+In zope.conf you can use the <clock-server> directive to define a schedule and url
+with the following data.
+<clock-server>
+ method /path_to_feedfolder/update_feed_items
+ period 3600 # seconds
+ user admin
+ password 123
+ host localhost:8080
+</clock-server>
+
+
+Tests
+-----
+
+The look-here-first test is the doctest at 'doc/testDocIntegrationTests.txt'.
+
+Testing is best done with zope's zopectl. 'instancemanager
+<projectname> --test feedfeeder' will do that for you if you've set
+up instancemanager. Otherwise 'bin/zopectl test -s
+Products.feedfeeder'.
+
85 Products/feedfeeder/__init__.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+
+# There are three ways to inject custom code here:
+#
+# - To set global configuration variables, create a file AppConfig.py.
+# This will be imported in config.py, which in turn is imported in
+# each generated class and in this file.
+# - To perform custom initialisation after types have been registered,
+# use the protected code section at the bottom of initialize().
+# - To register a customisation policy, create a file CustomizationPolicy.py
+# with a method register(context) to register the policy.
+
+from zLOG import LOG, INFO, DEBUG
+
+LOG('feedfeeder', DEBUG, 'Installing Product')
+
+try:
+ import CustomizationPolicy
+except ImportError:
+ CustomizationPolicy = None
+
+from Globals import package_home
+from Products.CMFCore import utils as cmfutils
+from Products.CMFCore import permissions
+from Products.CMFCore import DirectoryView
+from Products.CMFPlone.utils import ToolInit
+from Products.Archetypes.atapi import *
+from Products.Archetypes import listTypes
+from Products.Archetypes.utils import capitalize
+
+import os, os.path
+
+from Products.feedfeeder.config import *
+
+DirectoryView.registerDirectory('skins', product_globals)
+DirectoryView.registerDirectory('skins/feedfeeder',
+ product_globals)
+
+##code-section custom-init-head #fill in your manual code here
+from Products.GenericSetup import EXTENSION
+from Products.GenericSetup import profile_registry
+import Products.CMFPlone.interfaces
+##/code-section custom-init-head
+
+
+def initialize(context):
+ ##code-section custom-init-top #fill in your manual code here
+ ##/code-section custom-init-top
+
+ # imports packages and types for registration
+ import content
+ import interfaces
+
+ import utilities
+ import contenthandler
+
+ # Initialize portal content
+ content_types, constructors, ftis = process_types(
+ listTypes(PROJECTNAME),
+ PROJECTNAME)
+
+ cmfutils.ContentInit(
+ PROJECTNAME + ' Content',
+ content_types = content_types,
+ permission = DEFAULT_ADD_CONTENT_PERMISSION,
+ extra_constructors = constructors,
+ fti = ftis,
+ ).initialize(context)
+
+ # Apply customization-policy, if theres any
+ if CustomizationPolicy and hasattr(CustomizationPolicy, 'register'):
+ CustomizationPolicy.register(context)
+ print 'Customization policy for feedfeeder installed'
+
+ ##code-section custom-init-bottom #fill in your manual code here
+ profile_registry.registerProfile(
+ name='default',
+ title='Feedfeeder',
+ description='Profile for Feedfeeder',
+ path='profiles/default',
+ product='feedfeeder',
+ profile_type=EXTENSION,
+ for_=Products.CMFPlone.interfaces.IPloneSiteRoot)
+ ##/code-section custom-init-bottom
+
0 Products/feedfeeder/browser/__init__.py
No changes.
46 Products/feedfeeder/browser/configure.zcml
@@ -0,0 +1,46 @@
+<configure
+ xmlns="http://namespaces.zope.org/browser"
+ >
+
+ <page
+ name="update_feed_items"
+ class=".feed.UpdateFeedItems"
+ for="Products.feedfeeder.interfaces.container.IFeedsContainer"
+ allowed_interface=".feed.IUpdateFeedItems"
+ permission="cmf.ManagePortal"
+ />
+
+ <page
+ name="is_feedcontainer"
+ class=".feed.IsFeedContainer"
+ for="*"
+ attribute="is_feedcontainer"
+ permission="zope2.View"
+ />
+
+ <page
+ name="feed-folder.html"
+ for="Products.feedfeeder.interfaces.container.IFeedsContainer"
+ permission="zope2.View"
+ template="feed-folder.pt"
+ allowed_interface="Products.feedfeeder.interfaces.folderview.IFeedfeederFolderView"
+ class=".feed.FeedFolderView"
+ />
+
+ <page
+ name="feed-folder.html"
+ for="Products.ATContentTypes.interface.IATTopic"
+ permission="zope2.View"
+ template="feed-folder.pt"
+ allowed_interface="Products.feedfeeder.interfaces.folderview.IFeedfeederFolderView"
+ class=".feed.FeedFolderView"
+ />
+
+ <page
+ name="feed-item.html"
+ for="Products.feedfeeder.interfaces.item.IFeedItem"
+ permission="zope2.View"
+ template="feed-item.pt"
+ />
+
+</configure>
48 Products/feedfeeder/browser/feed-folder.pt
@@ -0,0 +1,48 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ metal:use-macro="context/@@standard_macros/view"
+ i18n:domain="feedfeeder">
+
+ <body>
+ <div metal:fill-slot="body"
+ tal:define="results view/item_list;
+ Batch python:modules['Products.CMFPlone'].Batch;
+ b_size request/b_size|python:20;b_start python:0;b_start request/b_start | b_start;
+ batch python:Batch(results, b_size, int(b_start), orphan=1);">
+
+ <div metal:use-macro="context/document_actions/macros/document_actions"></div>
+
+ <h1 tal:content="context/title_or_id"></h1>
+
+ <div metal:use-macro="context/document_byline/macros/byline"></div>
+
+ <p class="documentDescription"
+ tal:content="context/Description"
+ tal:condition="context/Description"></p>
+
+
+ <table cellpadding="0"
+ class="FeedListing">
+ <tbody valign="top">
+ <tr tal:repeat="child batch">
+ <td tal:content="python:toLocalizedTime(child['updated_date'], long_format=0)"
+ class="feed-date">
+ 2006-11-10
+ </td>
+ <td>
+ <a tal:attributes="href child/url"
+ tal:content="structure child/title"> My Title </a>
+ <div tal:condition="child/summary"
+ tal:content="structure child/summary"> My summary </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <!-- Navigation -->
+ <div metal:use-macro="here/batch_macros/macros/navigation" />
+ </div>
+ </body>
+</html>
115 Products/feedfeeder/browser/feed-item.pt
@@ -0,0 +1,115 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:metal="http://xml.zope.org/namespaces/metal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ metal:use-macro="context/@@standard_macros/view"
+ i18n:domain="feedfeeder">
+
+ <body>
+ <div metal:fill-slot="body"
+ tal:define="text context/getText;
+ creator context/Creator;">
+
+ <div metal:use-macro="context/document_actions/macros/document_actions"></div>
+
+ <h1 tal:content="structure context/title_or_id"></h1>
+
+ <div class="documentByLine">
+ <img src="" alt=""
+ title="This document is locked."
+ tal:define="locked portal/lock_icon.gif;"
+ tal:condition="isLocked"
+ tal:replace="structure python:locked.tag(title='Locked')"
+ i18n:attributes="title label_document_locked;"
+ />
+
+ <tal:name tal:condition="creator"
+ tal:define="author python:mtool.getMemberInfo(creator)">
+ <span i18n:translate="label_by_author">
+ by
+ <span tal:replace="context/getFeedItemAuthor"></span>
+ </span>
+
+ &mdash;
+
+ </tal:name>
+
+ <span i18n:translate="box_last_modified">
+ last modified
+ </span>
+ <span tal:replace="python:toLocalizedTime(context.getFeedItemUpdated(),long_format=0)">
+ August 16, 2001
+ </span>
+
+ <span class="state-expired"
+ tal:condition="python:portal.isExpired(here)"
+ i18n:translate="time_expired">
+ &mdash; expired
+ </span>
+
+ <span tal:define="locked portal/lock_icon.gif"
+ tal:condition="isLocked">
+ <img src=""
+ tal:replace="structure python:locked.tag(title='This item is locked')"
+ alt="Object locked"
+ i18n:attributes="alt label_object_locked;" />
+ </span>
+
+ <div tal:condition="here/Contributors"
+ i18n:translate="text_contributors">
+ Contributors:
+ <span i18n:name="name"
+ tal:omit-tag=""
+ tal:content="python: ', '.join(here.Contributors())">
+ Mary
+ </span>
+ </div>
+
+ <tal:rights condition="here/Rights">
+ <div tal:replace="here/Rights">
+ Copyleft NiceCorp Inc.
+ </div>
+ </tal:rights>
+
+ <span metal:use-macro="here/review_history/macros/review_history" />
+
+ </div>
+
+ <p class="documentDescription"
+ tal:content="structure context/Description"
+ tal:condition="context/Description"></p>
+
+ <p tal:condition="python: not text and is_editable"
+ i18n:translate="no_body_text"
+ class="discreet">
+ This item does not have any body text, click the edit tab to change it.
+ </p>
+
+ <div class="plain"
+ tal:condition="text">
+ <div tal:replace="structure text" />
+ </div>
+
+ <tal:enclosures condition="python:context.getFolderContents()">
+ <h3>Enclosures</h3>
+
+ <ul>
+ <li tal:repeat="child python:context.getFolderContents()">
+ <a tal:attributes="href child/getURL"
+ tal:content="child/Title"
+ href="">Enclosure</a>
+ </li>
+ </ul>
+ </tal:enclosures>
+
+ <div i18n:translate="source_available"
+ class="feed-source-link">
+ Source available
+ <a href="#"
+ tal:attributes="href context/remote_url"
+ i18n:attributes="name string:here"
+ i18n:translate="link_here">here</a>
+ </div>
+ </div>
+ </body>
+</html>
95 Products/feedfeeder/browser/feed.py
@@ -0,0 +1,95 @@
+from zope import interface
+from zope import component
+from Products.feedfeeder.interfaces.consumer import IFeedConsumer
+from Products.feedfeeder.interfaces.container import IFeedsContainer
+
+class IUpdateFeedItems(interface.Interface):
+ def update(): pass
+
+
+
+class IsFeedContainer(object):
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def is_feedcontainer(self):
+ return IFeedsContainer.providedBy(self.context)
+
+
+class UpdateFeedItems(object):
+ """A view for updating the feed items in a feed folder.
+ """
+
+ interface.implements(IUpdateFeedItems)
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def update(self):
+ consumer = component.getUtility(IFeedConsumer)
+ consumer.retrieveFeedItems(self.context)
+
+ def __call__(self):
+ self.update()
+ self.request.response.redirect(
+ self.context.absolute_url()
+ +"?portal_status_message=Feed+items+updated")
+
+
+class FeedFolderView(object):
+ """A view for feed folders.
+ """
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ @property
+ def items(self):
+ """Return all feed items.
+
+ Currently implemented as a generator since there could
+ theoretically be tens of thousands of items.
+ """
+
+ listing = self.context.getFolderContents
+ results = listing({'sort_on': 'getFeedItemUpdated',
+ 'sort_order': 'descending',
+ 'portal_type': 'FeedFeederItem'})
+ if not results and self.context.portal_type == 'Topic':
+ # Use the queryCatalog of the Topic itself.
+ results = self.context.queryCatalog(
+ portal_type='FeedFeederItem')
+ for index, x in enumerate(results):
+ content_url = x.getURL()
+ item = dict(updated_date = x.getFeedItemUpdated,
+ url = content_url,
+ content_url = content_url,
+ title = x.Title,
+ summary = x.Description,
+ author = x.getFeedItemAuthor,
+ has_text = x.getHasBody,
+ target_link = x.getLink,
+ )
+ self.extraDecoration(item, x)
+ enclosures = x.getObjectids
+
+ if (enclosures and enclosures is not None and
+ len(enclosures) == 1):
+ # only one enclosure? return item title but return link
+ # to sole enclosure, unless there is some body text.
+ if not int(x.getHasBody):
+ item['url'] = item['url'] + '/' + enclosures[0]
+ yield item
+
+ def extraDecoration(self, item, brain):
+ pass
+
+ def item_list(self):
+ return [x for x in self.items]
+
+ def __call__(self, *args, **kwargs):
+ return self.index(template_id='feed-folder.html')
58 Products/feedfeeder/config.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+
+# Product configuration.
+#
+# The contents of this module will be imported into __init__.py, the
+# workflow configuration and every content type module.
+#
+# If you wish to perform custom configuration, you may put a file
+# AppConfig.py in your product's root directory. This will be included
+# in this file if found.
+
+from Products.CMFCore.permissions import setDefaultRoles
+##code-section config-head #fill in your manual code here
+##/code-section config-head
+
+
+PROJECTNAME = "feedfeeder"
+
+# Check for Plone 2.1
+try:
+ from Products.CMFPlone.migrations import v2_1
+except ImportError:
+ HAS_PLONE21 = False
+else:
+ HAS_PLONE21 = True
+
+# Permissions
+#DEFAULT_ADD_CONTENT_PERMISSION = "Add portal content"
+DEFAULT_ADD_CONTENT_PERMISSION = "%s: Add" % PROJECTNAME
+setDefaultRoles(DEFAULT_ADD_CONTENT_PERMISSION, ('Manager', 'Owner'))
+
+product_globals = globals()
+
+# Dependencies of Products to be installed by quick-installer
+# override in custom configuration
+DEPENDENCIES = []
+
+# Dependend products - not quick-installed - used in testcase
+# override in custom configuration
+PRODUCT_DEPENDENCIES = []
+
+# You can overwrite these two in an AppConfig.py:
+# STYLESHEETS = [{'id': 'my_global_stylesheet.css'},
+# {'id': 'my_contenttype.css',
+# 'expression': 'python:object.getTypeInfo().getId() == "MyType"'}]
+# You can do the same with JAVASCRIPTS.
+STYLESHEETS = []
+JAVASCRIPTS = []
+
+##code-section config-bottom #fill in your manual code here
+##/code-section config-bottom
+
+
+# Load custom configuration not managed by ArchGenXML
+try:
+ from Products.feedfeeder.AppConfig import *
+except ImportError:
+ pass
31 Products/feedfeeder/configure.zcml
@@ -0,0 +1,31 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser"
+ xmlns:five="http://namespaces.zope.org/five"
+ xmlns:zcml="http://namespaces.zope.org/zcml">
+
+ <!-- zope.annotation is used in Zope 2.10 -->
+ <include
+ zcml:condition="installed zope.annotation"
+ package="zope.annotation" />
+ <!-- zope.app.annotation is used in Zope 2.9
+ We know that zope.app.annotation.tests is only installed there. -->
+ <include
+ zcml:condition="installed zope.app.annotation.tests"
+ package="zope.app.annotation" />
+
+ <utility
+ factory=".utilities.FeedConsumer"
+ permission="cmf.ModifyPortalContent"
+ provides=".interfaces.consumer.IFeedConsumer"
+ />
+
+ <adapter
+ for=".interfaces.item.IFeedItem"
+ provides=".interfaces.contenthandler.IFeedItemContentHandler"
+ factory=".contenthandler.StandardContentHandler"
+ />
+
+ <include package=".browser" />
+
+</configure>
16 Products/feedfeeder/content/__init__.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+
+##code-section init-module-header #fill in your manual code here
+##/code-section init-module-header
+
+
+# Subpackages
+# Additional
+
+# Classes
+import folder
+import item
+
+##code-section init-module-footer #fill in your manual code here
+##/code-section init-module-footer
+
159 Products/feedfeeder/content/folder.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+from AccessControl import ClassSecurityInfo
+from Products.Archetypes.atapi import *
+from zope import interface
+from Products.ATContentTypes.content.folder import ATBTreeFolder
+from Products.feedfeeder.interfaces.container import IFeedsContainer
+from Products.feedfeeder.config import *
+from Products.CMFCore.utils import getToolByName
+
+##code-section module-header #fill in your manual code here
+##/code-section module-header
+
+schema = Schema((
+
+ LinesField(
+ name='feeds',
+ widget=LinesWidget(
+ label='Feeds',
+ label_msgid='feedfeeder_label_feeds',
+ i18n_domain='feedfeeder',
+ )
+ ),
+
+ StringField(
+ name='defaultTransition',
+ vocabulary='getAvailableTransitions',
+ widget=SelectionWidget(
+ format='select',
+ description="When updating this feed's item the transition selected below will be performed.",
+ description_msgid="help_default_transition",
+ label='Default transition',
+ label_msgid='label_default_transition',
+ i18n_domain='feedfeeder',
+ )
+ ),
+),
+)
+
+##code-section after-local-schema #fill in your manual code here
+##/code-section after-local-schema
+
+FeedfeederFolder_schema = ATBTreeFolder.schema.copy() + \
+ schema.copy()
+
+##code-section after-schema #fill in your manual code here
+##/code-section after-schema
+
+class FeedfeederFolder(ATBTreeFolder):
+ """
+ """
+ security = ClassSecurityInfo()
+ # zope3 interfaces
+ interface.implements(IFeedsContainer)
+
+ # This name appears in the 'add' box
+ archetype_name = 'Feed Folder'
+
+ meta_type = 'FeedfeederFolder'
+ portal_type = 'FeedfeederFolder'
+ allowed_content_types = ['FeedFeederItem']
+ filter_content_types = 1
+ global_allow = 1
+ content_icon = 'feed_icon.gif'
+ immediate_view = 'feed-folder.html'
+ default_view = 'feed-folder.html'
+ suppl_views = ()
+ typeDescription = "Feed Folder"
+ typeDescMsgId = 'description_edit_feedfeederfolder'
+
+
+ actions = (
+
+
+ {'action': "string:${object_url}/update_feed_items",
+ 'category': "object_buttons",
+ 'id': 'update_feed_items',
+ 'name': 'Update Feed Items',
+ 'permissions': ("View",),
+ 'condition': 'python:1'
+ },
+
+
+ {'action': "string:$object_url/feed-folder.html",
+ 'category': "object",
+ 'id': 'view',
+ 'name': 'view',
+ 'permissions': ("View",),
+ 'condition': 'python:1'
+ },
+
+
+ )
+
+ _at_rename_after_creation = True
+
+ schema = FeedfeederFolder_schema
+
+ ##code-section class-header #fill in your manual code here
+ ##/code-section class-header
+
+ # Methods
+
+ # Methods from Interface IFeedsContainer
+
+ security.declarePublic('getItem')
+
+ def getAvailableTransitions(self):
+ # Create a temporary object so we can ask what transitions are
+ # available for it.
+ id = 'temp_zest_temp'
+ self.invokeFactory('FeedFeederItem', id)
+ wf_tool = getToolByName(self,'portal_workflow')
+ transitions = wf_tool.getTransitionsFor(self[id])
+ display_trans = [('', 'Keep initial state'),]
+ for trans in transitions:
+ display_trans.append( ( trans['id'], trans['name'] ) )
+ # Unindex and remove the temporary object
+ self[id].unindexObject()
+ self._delOb(id)
+ return DisplayList(display_trans)
+
+ def getItem(self,id):
+ """
+ """
+ if id in self.objectIds():
+ return self[id]
+ return None
+
+ security.declarePublic('getFeeds')
+ def getFeeds(self):
+ """
+ """
+ return self.feeds
+
+ security.declarePublic('replaceItem')
+ def replaceItem(self,id):
+ """
+ """
+ self.manage_delObjects([id])
+ return self.addItem(id)
+
+ security.declarePublic('addItem')
+ def addItem(self, id):
+ """
+ """
+ self.invokeFactory('FeedFeederItem', id)
+ transition = self.getDefaultTransition()
+ if transition != '':
+ wf_tool = getToolByName(self,'portal_workflow')
+ wf_tool.doActionFor(self[id], transition,
+ comment='Automatic transition triggered by FeedFolder')
+ return self[id]
+
+
+registerType(FeedfeederFolder, PROJECTNAME)
+# end of class FeedfeederFolder
+
+##code-section module-footer #fill in your manual code here
+##/code-section module-footer
164 Products/feedfeeder/content/item.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+from AccessControl import ClassSecurityInfo
+from Products.Archetypes.atapi import *
+from zope import interface
+from Products.ATContentTypes.content.folder import ATFolder
+from Products.feedfeeder.interfaces.item import IFeedItem
+from Products.feedfeeder.config import *
+from Products.CMFCore.utils import getToolByName
+
+from Products.ATContentTypes.content.document import ATDocument
+from DateTime import DateTime
+
+
+copied_fields = {}
+copied_fields['text'] = ATDocument.schema['text'].copy()
+copied_fields['text'].required = 0
+schema = Schema((
+
+ StringField(
+ name='feedItemAuthor',
+ widget=StringWidget(
+ label='Feeditemauthor',
+ label_msgid='feedfeeder_label_feedItemAuthor',
+ i18n_domain='feedfeeder',
+ )
+ ),
+
+ DateTimeField(
+ name='feedItemUpdated',
+ default=DateTime('2000/01/01'),
+ widget=CalendarWidget(
+ label='Feeditemupdated',
+ label_msgid='feedfeeder_label_feedItemUpdated',
+ i18n_domain='feedfeeder',
+ )
+ ),
+
+ copied_fields['text'],
+ StringField(
+ name='link',
+ widget=StringWidget(
+ label='Link',
+ label_msgid='feedfeeder_label_link',
+ i18n_domain='feedfeeder',
+ )
+ ),
+
+ ComputedField(
+ name='objectids',
+ widget=ComputedWidget(
+ label='Object Ids',
+ label_msgid='feedfeeder_label_objectids',
+ i18n_domain='feedfeeder',
+ )
+ ),
+
+ ComputedField(
+ name='hasBody',
+ widget=ComputedWidget(
+ label='Has body text',
+ label_msgid='feedfeeder_label_hasbody',
+ i18n_domain='feedfeeder',
+ )
+ ),
+
+ StringField(
+ name='feedTitle',
+ widget=StringWidget(
+ label='Feed Title',
+ label_msgid='feedfeeder_label_feedTitle',
+ i18n_domain='feedfeeder',
+ )
+ ),
+
+),
+)
+
+FeedFeederItem_schema = getattr(ATFolder, 'schema', Schema(())).copy() + \
+ schema.copy()
+
+
+class FeedFeederItem(ATFolder):
+ """
+ """
+ security = ClassSecurityInfo()
+ # zope3 interfaces
+ interface.implements(IFeedItem)
+
+ # This name appears in the 'add' box
+ archetype_name = 'Feed Item'
+
+ meta_type = 'FeedFeederItem'
+ portal_type = 'FeedFeederItem'
+ allowed_content_types = ['File']
+ filter_content_types = 1
+ global_allow = 0
+ #content_icon = 'FeedFeederItem.gif'
+ immediate_view = 'feed-item.html'
+ default_view = 'feed-item.html'
+ suppl_views = ()
+ typeDescription = "Feed Item"
+ typeDescMsgId = 'description_edit_feedfeederitem'
+
+
+ actions = (
+
+
+ {'action': "string:${object_url}/feed-item.html",
+ 'category': "object",
+ 'id': 'view',
+ 'name': 'view',
+ 'permissions': ("View",),
+ 'condition': 'python:1'
+ },
+
+
+ )
+
+ _at_rename_after_creation = True
+
+ schema = FeedFeederItem_schema
+
+
+ security.declarePublic('addEnclosure')
+ def addEnclosure(self,id):
+ """
+ """
+ self.invokeFactory('File', id)
+ self.reindexObject()
+ transition = self.getDefaultTransition()
+ if transition != '':
+ wf_tool = getToolByName(self,'portal_workflow')
+ # The default transition should be valid for a
+ # FeedFolderItem, but our File might not have the same
+ # transitions available. So check this.
+ transitions = wf_tool.getTransitionsFor(self[id])
+ transition_ids = [trans['id'] for trans in transitions]
+ if transition in transition_ids:
+ wf_tool.doActionFor(self[id], transition,
+ comment='Automatic transition triggered by FeedFolder')
+ return self[id]
+
+ security.declarePublic('remote_url')
+ def remote_url(self):
+ """Compatibility method that makes working with link checkers
+ easier.
+ """
+ return self.getLink()
+
+ security.declarePublic('getObjectids')
+ def getObjectids(self):
+ """Return the ids of enclosed objects.
+ """
+ return self.objectIds()
+
+ security.declarePublic('getHasBody')
+ def getHasBody(self):
+ """Return True if the object has body text.
+ """
+ if bool(self.getText()):
+ return 1
+ return 0
+
+registerType(FeedFeederItem, PROJECTNAME)
65 Products/feedfeeder/contenthandler.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+
+from Products.feedfeeder.interfaces.contenthandler import IFeedItemContentHandler
+from persistent.dict import PersistentDict
+from zope import interface
+try:
+ from zope.annotation.interfaces import IAttributeAnnotatable, IAnnotations
+except ImportError:
+ from zope.app.annotation.interfaces import IAttributeAnnotatable, IAnnotations
+
+
+class StandardContentHandler:
+ """
+ """
+ interface.implements(IFeedItemContentHandler)
+
+ def __init__(self, context):
+ self.context = context
+
+ def apply(self, contentNode):
+ self.context.update(text=contentNode.toxml())
+
+
+class AnnotationContentHandler(object):
+ """A content handler that parses definition list entries to apply
+ zope3 style annotations to the context.
+ """
+
+ interface.implements(IFeedItemContentHandler)
+
+ ANNO_KEY = 'feedfeeder.metadata'
+
+ def __init__(self, context):
+ self.context = context
+
+ def _extractText(self, node):
+ s = node.toxml().strip()
+ s = s[len(node.nodeName)+2:-1*(len(node.nodeName)+3)]
+ return s
+
+ def apply(self, contentNode):
+ if not IAttributeAnnotatable.providedBy(self.context):
+ directly = interface.directlyProvidedBy(self.context)
+ interface.directlyProvides(self.context,
+ directly + IAttributeAnnotatable)
+ annotations = IAnnotations(self.context)
+ metadata = annotations.get(self.ANNO_KEY, None)
+ if metadata is None:
+ metadata = PersistentDict()
+ annotations[self.ANNO_KEY] = metadata
+
+ for dl_el in contentNode.childNodes:
+ if dl_el.nodeName != 'dl':
+ continue
+
+ term = None
+ for el in dl_el.childNodes:
+ if el.nodeName == 'dt':
+ term = self._extractText(el)
+ elif el.nodeName == 'dd':
+ definition = self._extractText(el)
+ metadata[term] = definition
+
+
+
1 Products/feedfeeder/doc/__init__.py
@@ -0,0 +1 @@
+#
166 Products/feedfeeder/doc/feedconsuming.txt
@@ -0,0 +1,166 @@
+Consuming Feeds
+===============
+
+The basic premise here is that there is a global utility available that
+is capable of taking an object that either implements or adapts to
+the IFeedsContainer interface. And with that object, it can retrieve
+feed items and store them within the object ... typically as
+(but not restricted to) child items in a folderish container.
+
+Mock Container
+~~~~~~~~~~~~~~
+
+The requirements for getting feed information and populating feed items
+are pretty minimal. The class merely has to implement IFeedsContainer.
+
+ >>> from Products.feedfeeder.interfaces.container import IFeedsContainer
+ >>> from Products.feedfeeder.interfaces.item import IFeedItem
+ >>> import zope.interface
+
+ >>> class MockFeedsContainer(object):
+ ... zope.interface.implements(IFeedsContainer)
+ ... def __init__(self, feeds):
+ ... self.items = {}
+ ... self.feeds = feeds
+ ... def getFeeds(self):
+ ... return self.feeds
+ ... def addItem(self, id):
+ ... item = MockFeedItem()
+ ... self.items[id] = item
+ ... return item
+ ... def replaceItem(self, id):
+ ... return self.addItem(id)
+ ... def getItem(self,id):
+ ... return self.items.get(id, None)
+
+ >>> class MockFeedItem(object):
+ ... zope.interface.implements(IFeedItem)
+ ... def __init__(self):
+ ... self.enclosures = {}
+ ... def update(self, **kwargs):
+ ... for key, value in kwargs.items():
+ ... setattr(self, key, value)
+ ... def addEnclosure(self, id):
+ ... self.enclosures[id] = MockFeedEnclosure(id)
+ ... return self.enclosures[id]
+ ... def getText(self):
+ ... return self.text
+ ... def manage_renameObject(self, orig_id, new_id):
+ ... if new_id in self.enclosures.keys():
+ ... raise KeyError('Enclosure with %r id already exists' % repr(new_id))
+ ... self.enclosures[new_id] = self.enclosures[orig_id]
+ ... del self.enclosures[orig_id]
+ ... self.enclosures[new_id].id = new_id
+ ... def objectIds(self): return self.enclosures.keys()
+ ... def getFeedItemUpdated(self): return self.feedItemUpdated
+ ... def setFeedItemUpdated(self, x): self.feedItemUpdated = x
+ ... def getFeedItemAuthor(self): return self.feedItemAuthor
+ ... def setFeedItemAuthor(self, x): self.feedItemAuthor = x
+ ... def getFeedTitle(self): return self.feedTitle
+ ... def setFeedTitle(self, x): self.feedTitle = x
+ ... def setEffectiveDate(self, x): pass
+ ... def reindexObject(self): pass
+
+ >>> class MockFeedEnclosure(object):
+ ... def __init__(self, id):
+ ... self.id = id
+ ... def getId(self):
+ ... return self.id
+ ... def Title(self):
+ ... return getattr(self, 'title', '')
+ ... def update(self, **kwargs):
+ ... for key, value in kwargs.items():
+ ... setattr(self, key, value)
+ ... def size(self):
+ ... if hasattr(self, 'text'):
+ ... return len(self.text)
+ ... return len(getattr(self, 'data', ''))
+ ... def update_data(self, data, mime_type):
+ ... self.data = data
+ ... if hasattr(data, 'read'):
+ ... self.data = data.read()
+ ... self.mime_type = mime_type
+ ... def __str__(self):
+ ... return '<MockFeedEnclosure id=%s>' % self.id
+ ... __repr__ = __str__
+
+Using The Utility
+~~~~~~~~~~~~~~~~~
+
+Lets begin by getting the proper path to our example atom feed and setting
+up our mock feedscontainer.
+
+ >>> import os
+ >>> import Products.feedfeeder
+ >>> samplesdir = os.path.dirname(Products.feedfeeder.__file__)
+ >>> samplesdir = os.path.join(samplesdir, 'tests', 'samples')
+
+Make sure the feed consumer tells us if it has been given a bad file
+path.
+
+ >>> from Products.feedfeeder import utilities
+ >>> consumer = utilities.FeedConsumer()
+ >>> container = MockFeedsContainer(['file:///foobar.xml'])
+ >>> consumer.retrieveFeedItems(container)
+ Traceback (most recent call last):
+ ...
+ IOError: Couldn't locate '/foobar.xml'
+
+Now lets use our utilty to build the feed items.
+
+ >>> os.chdir(samplesdir)
+ >>> samplefiles = [os.path.join(samplesdir, 'samplefeed1.xml'),
+ ... os.path.join(samplesdir, 'samplefeed2.xml'),
+ ... os.path.join(samplesdir, 'samplefeed3.xml'),
+ ... os.path.join(samplesdir, 'samplefeed4.xml')]
+ >>> container = MockFeedsContainer(['file://'+x for x in samplefiles])
+ >>> consumer.retrieveFeedItems(container)
+ >>> len(container.items)
+ 6
+
+Make sure the content all matches up.
+
+ >>> test_doc = container.items['0cfbced08adbdc1f3bf30b4120371b7d']
+ >>> test_doc.title
+ u'Philips Nieuws 24 juli'
+ >>> test_doc.getFeedTitle()
+ u'Philips Research Eindhoven News (Special)'
+ >>> len(test_doc.enclosures)
+ 1
+ >>> test_doc.enclosures.values()[0].size()
+ 1000
+
+Lets check out the document's feed item metadata.
+
+ >>> test_doc.getFeedItemAuthor()
+ u'Miriam Mobach'
+ >>> test_doc.getFeedItemUpdated()
+ DateTime('2006/07/21 12:00:00 GMT+0')
+
+There are duplicate entries in both feed1 and feed2. In the case of the
+first duplicate, feed2's entry is later and thus should be the one we have.
+
+ >>> duplicate1 = container.items['649c8553c458001dbbb9b15957d58a92']
+ >>> duplicate1.text
+ u'<div>\nhello bar\n</div>'
+
+For the second duplicate, the first entry is the latest and should be
+kept.
+ >>> duplicate2 = container.items['30ca408a75537f05d03821c64473289e']
+ >>> duplicate2.text
+ u'<div>\nwoo hoo, a party!\n</div>'
+ >>> len(duplicate2.enclosures)
+ 2
+ >>> sorted([x.id for x in duplicate2.enclosures.values()])
+ [u'test2.doc', u'test3.xls']
+
+Get our item that talks about the thesis defence of Reinout.
+
+ >>> thesis = container.items['f065f774e8c28bf9f33221f290c4ca6a']
+ >>> thesis.title
+ u'Thesis defence: 15 January 2007 15:00'
+
+Now someone could have added an enclosure that is actually just a link. We append that link with an "a href" to the text of the item.
+
+ >>> thesis.getText()
+ u'<div>\nGreat news: a final thesis defence date!\n</div>'
291 Products/feedfeeder/doc/feedfeeder-integration.txt
@@ -0,0 +1,291 @@
+Feedfeeder Integration
+======================
+
+Feedfeeder uses feedparser to read in Atom feeds and to create new
+documents or files if there are new items in that feed. Those new
+items are created inside the feedfeederfolder itself, as it is a
+folderish type.
+
+Lets begin by creating our feeder.
+
+ >>> id = folder.invokeFactory('FeedfeederFolder', 'feeder')
+ >>> feeder = folder.feeder
+
+Setup some feeds.
+
+ >>> import os
+ >>> import Products.feedfeeder
+
+ >>> samplesdir = os.path.dirname(Products.feedfeeder.__file__)
+ >>> samplesdir = os.path.join(samplesdir, 'tests', 'samples')
+ >>> os.chdir(samplesdir)
+ >>> samplefiles = [os.path.join(samplesdir, 'samplefeed1.xml'),
+ ... os.path.join(samplesdir, 'samplefeed2.xml')]
+ >>> feeder.setFeeds(['file://'+x for x in samplefiles])
+
+
+Available Transitions
+---------------------
+
+Feed items that are added during an update can be automatically
+transitioned.
+
+Between Plone 2.5 and 3.0 there were some changes to the workflows
+which make it tricky to have tests here that pass in both cases. We
+explicitly set the workflow chain for FeedFeederItems to
+plone_workflow, as that is what the default for Plone 2.5. And we do
+the same for Files, as we use them for enclosures::
+
+ >>> portal.portal_workflow.setChainForPortalTypes(['FeedFeederItem', 'File'], 'plone_workflow')
+
+See what transitions we have available::
+
+ >>> feeder.getAvailableTransitions()
+ <DisplayList [('', 'Keep initial state'), ('hide', 'Make private'), ('submit', 'Submit...>
+ >>> self.setRoles('Manager')
+ >>> [trans for trans in feeder.getAvailableTransitions()]
+ ['', 'hide', 'submit', 'publish']
+
+ >>> from Products.CMFCore.utils import getToolByName
+ >>> wf_tool = getToolByName(folder,'portal_workflow')
+ >>> wf_tool.doActionFor(feeder, 'publish')
+ >>> [trans for trans in feeder.getAvailableTransitions()]
+ ['', 'hide', 'submit', 'publish']
+ >>> self.setRoles('')
+ >>> [trans for trans in feeder.getAvailableTransitions()]
+ ['', 'hide', 'submit']
+
+
+Using Updating View
+-------------------
+
+ By default only Managers (with the ManagePortal permission) can
+ update FeedFolders.
+
+ >>> self.setRoles('Manager')
+ >>> feeder.setDefaultTransition('publish')
+ >>> view = feeder.restrictedTraverse('@@update_feed_items')
+ >>> view.update()
+ >>> self.setRoles('')
+
+Make sure we got what we wanted.
+
+ >>> sorted([x for x in feeder.objectIds()])
+ ['0cfbced08adbdc1f3bf30b4120371b7d', '30ca408a75537f05d03821c64473289e', '649c8553c458001dbbb9b15957d58a92', 'c17db5a7fa227e2e34193c71a173dbb1']
+
+ >>> test_doc = feeder['30ca408a75537f05d03821c64473289e']
+ >>> test_doc.title
+ u'Party!'
+ >>> test_doc.Description()
+ 'Party on the roof of the Mediterranean Inn'
+ >>> test_doc.getText()
+ '<div>\nwoo hoo, a party!\n</div>'
+ >>> enclosure = test_doc.objectValues()[0]
+ >>> enclosure
+ <ATFile at ...>
+
+And did the automatic transition work?
+For the FeedFeederItem:
+
+ >>> chain = wf_tool.getChainFor(test_doc)
+ >>> status = wf_tool.getStatusOf(chain[0], test_doc)
+ >>> status['review_state']
+ 'published'
+ >>> status['comments']
+ 'Automatic transition triggered by FeedFolder'
+
+For the enclosure:
+
+ >>> chain = wf_tool.getChainFor(enclosure)
+ >>> status = wf_tool.getStatusOf(chain[0], enclosure)
+ >>> status['review_state']
+ 'published'
+ >>> status['comments']
+ 'Automatic transition triggered by FeedFolder'
+
+
+
+Folder Listing
+--------------
+
+We need to make sure the items listed by the folder listing view match
+up with appropriate logic.
+
+ >>> id = folder.invokeFactory('FeedfeederFolder', 'feeder2')
+ >>> feeder = folder.feeder2
+ >>> view = feeder.restrictedTraverse('@@feed-folder.html')
+
+The requirements for how things get linked is as follows:
+
+ 1) feed item with no text and no enclosures: show feed item title and
+ link to feed item
+ 2) feed item with text: show feed item title and link to feed item
+ 3) feed item with text and one enclosure: show feed item title and
+ link to feed item
+ 4) feed item with no text and one enclosure: show feed item title and
+ link to enclosure
+ 5) feed item with no text and multiple enclosures: show feed item
+ title and link to feed item
+
+We have to satisfy each of these requirements. Make sure that a feed
+item with no text and no enclosures still have the feed item title and
+feed item url.
+
+ >>> id = feeder.invokeFactory('FeedFeederItem', '1')
+ >>> feeder[id].update(title='foo')
+ >>> items = [x for x in view.items]
+ >>> len(items)
+ 1
+ >>> items[0]['title']
+ 'foo'
+ >>> items[0]['url']
+ 'http://nohost/plone/Members/test_user_1_/feeder2/1'
+
+A feed item with text should have it's title and url displayed.
+
+ >>> feeder[id].setText('abcdef')
+ >>> items = [x for x in view.items]
+ >>> len(items)
+ 1
+ >>> items[0]['title']
+ 'foo'
+ >>> items[0]['url']
+ 'http://nohost/plone/Members/test_user_1_/feeder2/1'
+
+Even if the feed item has an enclosure, as long as it has text the
+feed item title and url should be used.
+
+ >>> obj = feeder[id].addEnclosure('1.1')
+ >>> obj.update(title='foo.bar')
+ >>> items = [x for x in view.items]
+ >>> len(items)
+ 1
+ >>> items[0]['title']
+ 'foo'
+ >>> items[0]['url']
+ 'http://nohost/plone/Members/test_user_1_/feeder2/1'
+
+If there is no text in the feed item and there is one and only one
+enclosure, then the feed item title and enclosure url should be used.
+
+ >>> feeder[id].setText('')
+ >>> feeder[id].reindexObject()
+ >>> len(feeder[id].getFolderContents())
+ 1
+ >>> items = [x for x in view.items]
+ >>> len(items)
+ 1
+ >>> items[0]['title']
+ 'foo'
+ >>> items[0]['url']
+ 'http://nohost/plone/Members/test_user_1_/feeder2/1/1.1'
+
+If there is no text in the feed item and there is more than one
+enclosure, we should go back to simply using the title and url of the
+feed item.
+
+ >>> feeder[id].setText('')
+ >>> obj = feeder[id].addEnclosure('1.2')
+ >>> obj.update(title='foo.hello')
+ >>> len(feeder[id].getFolderContents()) > 1
+ True
+ >>> items = [x for x in view.items]
+ >>> len(items)
+ 1
+ >>> items[0]['title']
+ 'foo'
+ >>> items[0]['url']
+ 'http://nohost/plone/Members/test_user_1_/feeder2/1'
+
+Items with summaries should have summaries. Summaries come from
+the description field on the content object.
+
+ >>> feeder[id].setDescription('Test Summary')
+ >>> feeder[id].update(title='blatant')
+ >>> items = [x for x in view.items]
+ >>> items[0]['summary']
+ 'Test Summary'
+
+Annotated Metadata Handler
+--------------------------
+
+Feedfeeder comes with an unregistered zope3 annotations based metadata
+handler adapter. Essentially it scans the content of an atom entry
+for a toplevel DL entry and saves the DT/DD items as annotation
+values. Lets make sure that works.
+
+ >>> from zope import component
+ >>> from Products.feedfeeder.interfaces.contenthandler import IFeedItemContentHandler
+ >>> from Products.feedfeeder.contenthandler import AnnotationContentHandler
+ >>> from Products.feedfeeder.interfaces.item import IFeedItem
+
+ >>> component.provideAdapter(AnnotationContentHandler,
+ ... adapts=(IFeedItem,),
+ ... provides=IFeedItemContentHandler,
+ ... name=u'definition-list-metadata')
+
+ >>> feeder.setFeeds(['file://'+os.path.join(samplesdir, 'samplefeed1.xml')])
+ >>> self.setRoles('Manager')
+ >>> view = feeder.restrictedTraverse('@@update_feed_items')
+ >>> view.update()
+ >>> self.setRoles('')
+
+Now that we've retrieved the items, lets make sure the metadata we expect
+is present.
+
+ >>> try:
+ ... from zope.annotation import IAnnotations
+ ... except ImportError:
+ ... from zope.app.annotation import IAnnotations
+ >>> annotations = IAnnotations(feeder['649c8553c458001dbbb9b15957d58a92'])
+ >>> metadata = annotations[AnnotationContentHandler.ANNO_KEY]
+ >>> dict(metadata)
+ {u'date': u'2006-03-23'}
+
+Also make sure that the normal content is still available, but that
+the dl with the extra data is gone.
+
+ >>> item = feeder['649c8553c458001dbbb9b15957d58a92']
+ >>> text = item.getText()
+ >>> 'definition-list-metadata' not in text
+ True
+ >>> 'the real text body here' in text
+ True
+
+Events
+------
+
+After a feed item has been consumed it fires an appropriate event to
+signify this. First lets clean up the feeder.
+
+ >>> feeder.manage_delObjects([x for x in feeder.objectIds()])
+
+Make sure that event is being properly fired and handled.
+
+ >>> class Handler:
+ ... event_obj = None
+ ... event_evt = None
+ ... def handle(self, event, obj):
+ ... self.event_obj = event
+ ... self.event_evt = obj
+ >>> handler = Handler()
+
+ >>> from zope import component
+ >>> from Products.feedfeeder.interfaces.item import IFeedItemConsumedEvent
+ >>> component.provideHandler(handler.handle,
+ ... (IFeedItem, IFeedItemConsumedEvent))
+
+Now that the event handler has been setup, parse in some entries and
+make sure the handler is getting to handle them.
+
+ >>> self.setRoles('Manager')
+ >>> view = feeder.restrictedTraverse('@@update_feed_items')
+ >>> view.update()
+ >>> self.setRoles('')
+
+ >>> handler.event_evt
+ <Products.feedfeeder.events.FeedItemConsumedEvent ...>
+ >>> handler.event_obj
+ <FeedFeederItem ...>
+
+
16 Products/feedfeeder/events.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+
+from zope import interface
+try:
+ from zope.lifecycleevent import ObjectModifiedEvent
+except ImportError:
+ # BBB for Zope 2.9
+ from zope.app.event.objectevent import ObjectModifiedEvent
+from Products.feedfeeder.interfaces import item as itemifaces
+
+
+class FeedItemConsumedEvent(ObjectModifiedEvent):
+ """Fired when a feed item has been successfully consumed.
+ """
+
+ interface.implements(itemifaces.IFeedItemConsumedEvent)
2,864 Products/feedfeeder/feedparser.py
2,864 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
43 Products/feedfeeder/i18n/feedfeeder-de.po
@@ -0,0 +1,43 @@
+# ArchGenXML generated POT File
+# Reinout van Rees <reinout@zestsoftware.nl>, 2006.
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: 2006-10-19 14:47+0200\n"
+"Last-Translator: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"Language-Team: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: de\n"
+"Language-Name: German\n"
+"Preferred-Encodings: latin1 utf-8\n"
+"Domain: feedfeeder\n"
+
+#. Default: "Feeditemauthor"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemAuthor"
+msgstr "Autor(in)"
+
+#. Default: "Feeditemupdated"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemUpdated"
+msgstr "Letzte Aktualisierung"
+
+#. Default: "Feeds"
+#: content/FeedfeederFolder.py
+msgid "feedfeeder_label_feeds"
+msgstr "URLs der Feeds"
+
+#. Default: "Links"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_link"
+msgstr "URL des Originals"
+
+#. Default: "Test view showing the latest items from the feeds"
+#: skins/feedfeeder/latest_items.pt
+msgid "test_view_title"
+msgstr "Die aktuelste einträge der Feeds (nur als Test)"
+
43 Products/feedfeeder/i18n/feedfeeder-eu.po
@@ -0,0 +1,43 @@
+# ArchGenXML generated POT File
+# Reinout van Rees <reinout@zestsoftware.nl>, 2006.
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: 2007-02-16 15:17+0100\n"
+"Last-Translator: Mikel Larreategi <mlarreategi@codesyntax.com>\n"
+"Language-Team: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: eu\n"
+"Language-Name: Basque\n"
+"Preferred-Encodings: latin1 utf-8\n"
+"Domain: feedfeeder\n"
+
+#. Default: "Feeditemauthor"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemAuthor"
+msgstr "Egilea"
+
+#. Default: "Feeditemupdated"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemUpdated"
+msgstr "Eguneraketa data"
+
+#. Default: "Feeds"
+#: content/FeedfeederFolder.py
+msgid "feedfeeder_label_feeds"
+msgstr "Jarioak"
+
+#. Default: "Links"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_link"
+msgstr "Loturak"
+
+#. Default: "Test view showing the latest items from the feeds"
+#: skins/feedfeeder/latest_items.pt
+msgid "test_view_title"
+msgstr "Jarioetako azken elementuak erakusten dituen bista."
+
53 Products/feedfeeder/i18n/feedfeeder-fr.po
@@ -0,0 +1,53 @@
+# ArchGenXML generated POT File
+# Reinout van Rees <reinout@zestsoftware.nl>, 2006.
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: Jul 11 09:13:40 2007\n"
+"Last-Translator: Thomas Desvenain <thomas.desvenain@ingeniweb.com>\n"
+"Language-Team: Ingeniweb <thomas.desvenain@ingeniweb.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: fr\n"
+"Language-Name: French\n"
+"Preferred-Encodings: utf-8\n"
+"Domain: feedfeeder\n"
+
+#. Default: "Feeditemauthor"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemAuthor"
+msgstr "Auteur du flux"
+
+#. Default: "Feeditemupdated"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemUpdated"
+msgstr "Flux Rss rafraîchi"
+
+#. Default: "Feeds"
+#: content/FeedfeederFolder.py
+msgid "feedfeeder_label_feeds"
+msgstr "Flux"
+
+#. 'Default transition'
+#: content/folder.py
+msgid "label_default_transition"
+msgstr "Transition par défaut"
+
+#. "When updating this feed's item the transition selected below will be performed."
+#: content/folder.py
+msgid "help_default_transition"
+msgstr "La transition effectuée lorsque vous mettez à jour ce flux"
+
+#. Default: "Links"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_link"
+msgstr "Liens"
+
+#. Default: "Test view showing the latest items from the feeds"
+#: skins/feedfeeder/latest_items.pt
+msgid "test_view_title"
+msgstr "Vue affichant les derniers items du flux"
+
43 Products/feedfeeder/i18n/feedfeeder-nl.po
@@ -0,0 +1,43 @@
+# ArchGenXML generated POT File
+# Reinout van Rees <reinout@zestsoftware.nl>, 2006.
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: 2006-10-19 14:47+0200\n"
+"Last-Translator: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"Language-Team: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: nl\n"
+"Language-Name: Dutch\n"
+"Preferred-Encodings: latin1 utf-8\n"
+"Domain: feedfeeder\n"
+
+#. Default: "Feeditemauthor"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemAuthor"
+msgstr "Auteur"
+
+#. Default: "Feeditemupdated"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemUpdated"
+msgstr "Laatste wijziging"
+
+#. Default: "Feeds"
+#: content/FeedfeederFolder.py
+msgid "feedfeeder_label_feeds"
+msgstr "URLs van de feeds"
+
+#. Default: "Links"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_link"
+msgstr "URL (van het oorspronkelijke item)"
+
+#. Default: "Test view showing the latest items from the feeds"
+#: skins/feedfeeder/latest_items.pt
+msgid "test_view_title"
+msgstr "Test overzicht: de laatste items uit de feeds"
+
21 Products/feedfeeder/i18n/feedfeeder-plone-de.po
@@ -0,0 +1,21 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: Thu Jul 20 09:13:40 2006\n"
+"Last-Translator: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"Language-Team: Reinout van Rees <reinout@zestsoftware.nl>\n Mirella van Teulingen <m.van.teulingen@zestsoftware.nl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: de\n"
+"Language-Name: Deutsch\n"
+"Preferred-Encodings: latin1 utf-8\n"
+"Domain: plone\n"
+
+msgid "Feed folder"
+msgstr "Feed Ordner"
+
+msgid "Feed Item"
+msgstr ""
22 Products/feedfeeder/i18n/feedfeeder-plone-eu.po
@@ -0,0 +1,22 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: 2007-02-16 15:13+0100\n"
+"Last-Translator: Mikel Larreategi <mlarreategi@codesyntax.com>\n"
+"Language-Team: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: eu\n"
+"Language-Name: Basque\n"
+"Preferred-Encodings: latin1 utf-8\n"
+"Domain: plone\n"
+
+msgid "Feed folder"
+msgstr "Jario karpeta"
+
+msgid "Feed Item"
+msgstr "Jario elementua"
+
25 Products/feedfeeder/i18n/feedfeeder-plone-fr.po
@@ -0,0 +1,25 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: Jul 11 09:13:40 2007\n"
+"Last-Translator: Thomas Desvenain <thomas.desvenain@ingeniweb.com>\n"
+"Language-Team: Ingeniweb <thomas.desvenain@ingeniweb.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: fr\n"
+"Language-Name: French\n"
+"Preferred-Encodings: utf-8\n"
+"Domain: plone\n"
+
+msgid "Feed folder"
+msgstr "Dossier de Flux Rss"
+
+
+msgid "Feed Folder"
+msgstr "Dossier de Flux Rss"
+
+msgid "Feed Item"
+msgstr "Element Rss"
21 Products/feedfeeder/i18n/feedfeeder-plone-nl.po
@@ -0,0 +1,21 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: Thu Jul 20 09:13:40 2006\n"
+"Last-Translator: Mirella van Teulingen <m.van.teulingen@zestsoftware.nl>\n"
+"Language-Team: Reinout van Rees <reinout@zestsoftware.nl>\n Mirella van Teulingen <m.van.teulingen@zestsoftware.nl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: nl\n"
+"Language-Name: Nederlands\n"
+"Preferred-Encodings: latin1 utf-8\n"
+"Domain: plone\n"
+
+msgid "Feed folder"
+msgstr "Feedmap"
+
+msgid "Feed Item"
+msgstr "Feed Item"
21 Products/feedfeeder/i18n/feedfeeder-plone.pot
@@ -0,0 +1,21 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: Thu Jul 20 09:13:40 2006\n"
+"Last-Translator: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"Language-Team: Reinout van Rees <reinout@zestsoftware.nl>\n Mirella van Teulingen <m.van.teulingen@zestsoftware.nl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: en\n"
+"Language-Name: English\n"
+"Preferred-Encodings: latin1 utf-8\n"
+"Domain: plone\n"
+
+msgid "Feed folder"
+msgstr ""
+
+msgid "Feed Item"
+msgstr ""
52 Products/feedfeeder/i18n/feedfeeder.pot
@@ -0,0 +1,52 @@
+# ArchGenXML generated POT File
+# Reinout van Rees <reinout@zestsoftware.nl>, 2006.
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: 2006-10-19 12:46+0000\n"
+"PO-Revision-Date: Thu Jul 20 09:13:40 2006\n"
+"Last-Translator: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"Language-Team: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: en\n"
+"Language-Name: English\n"
+"Preferred-Encodings: latin1 utf-8\n"
+"Domain: feedfeeder\n"
+
+#. Default: "Feeditemauthor"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemAuthor"
+msgstr ""
+
+#. Default: "Feeditemupdated"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemUpdated"
+msgstr ""
+
+#. Default: "Feeds"
+#: content/FeedfeederFolder.py
+msgid "feedfeeder_label_feeds"
+msgstr ""
+
+#. Default: "Links"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_link"
+msgstr ""
+
+#. Default: "Test view showing the latest items from the feeds"
+#: skins/feedfeeder/latest_items.pt
+msgid "test_view_title"
+msgstr ""
+
+#. 'Default transition'
+#: content/folder.py
+msgid "label_default_transition"
+msgstr ""
+
+#. "When updating this feed's item the transition selected below will be performed."
+#: content/folder.py
+msgid "help_default_transition"
+msgstr ""
38 Products/feedfeeder/i18n/generated.pot
@@ -0,0 +1,38 @@
+# ArchGenXML generated POT File
+# Reinout van Rees <reinout@zestsoftware.nl>, 2006.
+msgid ""
+msgstr ""
+"Project-Id-Version: feedfeeder\n"
+"POT-Creation-Date: Thu Jul 20 09:13:40 2006\n"
+"PO-Revision-Date: Thu Jul 20 09:13:40 2006\n"
+"Last-Translator: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"Language-Team: Reinout van Rees <reinout@zestsoftware.nl>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"Language-Code: en\n"
+"Language-Name: English\n"
+"Preferred-Encodings: latin1 utf-8\n"
+"Domain: feedfeeder\n"
+
+#. Default: "Feeditemauthor"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemAuthor"
+msgstr ""
+
+#. Default: "Feeditemupdated"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_feedItemUpdated"
+msgstr ""
+
+#. Default: "Feeds"
+#: content/FeedfeederFolder.py
+msgid "feedfeeder_label_feeds"
+msgstr ""
+
+#. Default: "Links"
+#: content/FeedFeederItem.py
+msgid "feedfeeder_label_link"
+msgstr ""
+
19 Products/feedfeeder/interfaces/__init__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+##code-section init-module-header #fill in your manual code here
+##/code-section init-module-header
+
+
+# Subpackages
+# Additional
+
+# Classes
+import container
+import consumer
+import item
+import contenthandler
+import folderview
+
+##code-section init-module-footer #fill in your manual code here
+##/code-section init-module-footer
+
31 Products/feedfeeder/interfaces/consumer.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+##code-section module-header #fill in your manual code here
+##/code-section module-header
+
+
+
+
+from zope import interface
+
+class IFeedConsumer(interface.Interface):
+ ''' '''
+
+ ##code-section class-header_IFeedConsumer #fill in your manual code here
+ ##/code-section class-header_IFeedConsumer
+
+
+
+
+ def retrieveFeedItems(container):
+ """
+
+ """
+
+
+
+##code-section module-footer #fill in your manual code here
+##/code-section module-footer
+
+
+
52 Products/feedfeeder/interfaces/container.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+##code-section module-header #fill in your manual code here
+##/code-section module-header
+
+
+
+
+from zope import interface
+
+class IFeedsContainer(interface.Interface):
+ ''' '''
+
+ ##code-section class-header_IFeedsContainer #fill in your manual code here
+ ##/code-section class-header_IFeedsContainer
+
+
+
+
+ def getFeeds():
+ """
+
+ """
+
+
+
+ def addItem(id):
+ """
+
+ """
+
+
+
+ def replaceItem(id):
+ """
+
+ """
+
+
+
+ def getItem(id):
+ """
+
+ """
+
+
+
+##code-section module-footer #fill in your manual code here
+##/code-section module-footer
+
+
+
31 Products/feedfeeder/interfaces/contenthandler.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+##code-section module-header #fill in your manual code here
+##/code-section module-header
+
+
+
+
+from zope import interface
+
+class IFeedItemContentHandler(interface.Interface):
+ ''' '''
+
+ ##code-section class-header_IFeedItemContentHandler #fill in your manual code here