You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by tv...@apache.org on 2013/05/08 00:27:48 UTC

[10/50] [abbrv] git commit: [#6172] Refactor feed functionality into mixin controller

[#6172] Refactor feed functionality into mixin controller

Signed-off-by: Tim Van Steenburgh <tv...@gmail.com>


Project: http://git-wip-us.apache.org/repos/asf/incubator-allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-allura/commit/391aa922
Tree: http://git-wip-us.apache.org/repos/asf/incubator-allura/tree/391aa922
Diff: http://git-wip-us.apache.org/repos/asf/incubator-allura/diff/391aa922

Branch: refs/heads/tv/3854
Commit: 391aa92294b69a228cee4b857c65e0d780ee0a36
Parents: 7244932
Author: Tim Van Steenburgh <tv...@gmail.com>
Authored: Tue Apr 30 17:44:37 2013 +0000
Committer: Dave Brondsema <db...@slashdotmedia.com>
Committed: Tue Apr 30 19:45:59 2013 +0000

----------------------------------------------------------------------
 Allura/allura/controllers/basetest_project_root.py |    2 -
 Allura/allura/controllers/discuss.py               |   75 ++++-------
 Allura/allura/controllers/feed.py                  |  108 +++++++++++++++
 Allura/allura/controllers/project.py               |   39 ++----
 Allura/allura/controllers/repository.py            |   31 +----
 Allura/allura/ext/user_profile/user_main.py        |   55 ++------
 ForgeBlog/forgeblog/main.py                        |   62 ++-------
 .../forgediscussion/controllers/root.py            |   44 ++----
 ForgeSVN/forgesvn/controllers.py                   |    3 +-
 ForgeSVN/forgesvn/svn_main.py                      |    3 -
 ForgeTracker/forgetracker/tracker_main.py          |   64 ++-------
 ForgeWiki/forgewiki/wiki_main.py                   |   67 ++-------
 12 files changed, 219 insertions(+), 334 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/Allura/allura/controllers/basetest_project_root.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/basetest_project_root.py b/Allura/allura/controllers/basetest_project_root.py
index f9ec3d5..8c48ed6 100644
--- a/Allura/allura/controllers/basetest_project_root.py
+++ b/Allura/allura/controllers/basetest_project_root.py
@@ -66,8 +66,6 @@ class BasetestProjectRootController(WsgiDispatchController, ProjectController):
     '''
 
     def __init__(self):
-        setattr(self, 'feed.rss', self.feed)
-        setattr(self, 'feed.atom', self.feed)
         for n in M.Neighborhood.query.find():
             if n.url_prefix.startswith('//'): continue
             n.bind_controller(self)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/Allura/allura/controllers/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/discuss.py b/Allura/allura/controllers/discuss.py
index 5abb3a2..b37d880 100644
--- a/Allura/allura/controllers/discuss.py
+++ b/Allura/allura/controllers/discuss.py
@@ -38,6 +38,7 @@ from allura.lib.helpers import DateTimeConverter
 
 from allura.lib.widgets import discuss as DW
 from .attachments import AttachmentsController, AttachmentController
+from .feed import Feed, FeedController
 
 log = logging.getLogger(__name__)
 
@@ -68,13 +69,11 @@ class WidgetConfig(object):
     thread_header = DW.ThreadHeader()
 
 # Controllers
-class DiscussionController(BaseController):
+class DiscussionController(BaseController, FeedController):
     M=ModelConfig
     W=WidgetConfig
 
     def __init__(self):
-        setattr(self, 'feed.rss', self.feed)
-        setattr(self, 'feed.atom', self.feed)
         if not hasattr(self, 'ThreadController'):
             self.ThreadController = ThreadController
         if not hasattr(self, 'PostController'):
@@ -106,29 +105,18 @@ class DiscussionController(BaseController):
             M.session.artifact_orm_session._get().skip_mod_date = True
         redirect(request.referer)
 
-    @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=DateTimeConverter(if_empty=None),
-            until=DateTimeConverter(if_empty=None),
-            page=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, page=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        title = 'Recent posts to %s' % self.discussion.name
-        feed = M.Feed.feed(
+    def get_feed(self, project, app, user):
+        """Return a :class:`allura.controllers.feed.Feed` object describing
+        the xml feed for this controller.
+
+        Overrides :meth:`allura.controllers.feed.FeedController.get_feed`.
+
+        """
+        return Feed(
             dict(ref_id={'$in': [t.index_id() for t in self.discussion.threads]}),
-            feed_type,
-            title,
-            self.discussion.url(),
-            title,
-            since, until, page, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
+            'Recent posts to %s' % self.discussion.name,
+            self.discussion.url())
+
 
 class AppDiscussionController(DiscussionController):
 
@@ -156,7 +144,7 @@ class ThreadsController(BaseController):
         else:
             raise exc.HTTPNotFound()
 
-class ThreadController(BaseController):
+class ThreadController(BaseController, FeedController):
     __metaclass__=h.ProxiedAttrMeta
     M=h.attrproxy('_discussion_controller', 'M')
     W=h.attrproxy('_discussion_controller', 'W')
@@ -170,8 +158,6 @@ class ThreadController(BaseController):
             require_access(self.thread.ref.artifact, 'read')
 
     def __init__(self, discussion_controller, thread_id):
-        setattr(self, 'feed.rss', self.feed)
-        setattr(self, 'feed.atom', self.feed)
         self._discussion_controller = discussion_controller
         self.discussion = discussion_controller.discussion
         self.thread = self.M.Thread.query.get(_id=thread_id)
@@ -237,29 +223,18 @@ class ThreadController(BaseController):
         flash('Thread flagged as spam.')
         redirect(self.discussion.url())
 
-    @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=DateTimeConverter(if_empty=None),
-            until=DateTimeConverter(if_empty=None),
-            page=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, page=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        title = 'Recent posts to %s' % (self.thread.subject or '(no subject)')
-        feed = M.Feed.feed(
+    def get_feed(self, project, app, user):
+        """Return a :class:`allura.controllers.feed.Feed` object describing
+        the xml feed for this controller.
+
+        Overrides :meth:`allura.controllers.feed.FeedController.get_feed`.
+
+        """
+        return Feed(
             dict(ref_id=self.thread.index_id()),
-            feed_type,
-            title,
-            self.thread.url(),
-            title,
-            since, until, page, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
+            'Recent posts to %s' % (self.thread.subject or '(no subject)'),
+            self.thread.url())
+
 
 class PostController(BaseController):
     __metaclass__=h.ProxiedAttrMeta

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/Allura/allura/controllers/feed.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/feed.py b/Allura/allura/controllers/feed.py
new file mode 100644
index 0000000..f014b21
--- /dev/null
+++ b/Allura/allura/controllers/feed.py
@@ -0,0 +1,108 @@
+#       Licensed to the Apache Software Foundation (ASF) under one
+#       or more contributor license agreements.  See the NOTICE file
+#       distributed with this work for additional information
+#       regarding copyright ownership.  The ASF licenses this file
+#       to you under the Apache License, Version 2.0 (the
+#       "License"); you may not use this file except in compliance
+#       with the License.  You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#       Unless required by applicable law or agreed to in writing,
+#       software distributed under the License is distributed on an
+#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#       KIND, either express or implied.  See the License for the
+#       specific language governing permissions and limitations
+#       under the License.
+
+from tg import expose, validate, request, response
+from tg.decorators import without_trailing_slash
+from formencode import validators as V
+from pylons import tmpl_context as c
+from webob import exc
+
+from allura import model as M
+from allura.lib import helpers as h
+
+
+class Feed(object):
+    """A facade for the arguments required by
+    :meth:`allura.model.artifact.Feed.feed`.
+
+    Used by :meth:`FeedController.feed` to create a real feed.
+
+    """
+    def __init__(self, query, title, url, description=None):
+        self.query = query
+        self.title = title
+        self.url = url
+        self.description = description or title
+
+
+class FeedController(object):
+    """Mixin class which adds RSS and Atom feed endpoints to an existing
+    controller.
+
+    Feeds will be accessible at the following URLs:
+
+        http://host/path/to/controller/feed -> RSS
+        http://host/path/to/controller/feed.rss -> RSS
+        http://host/path/to/controller/feed.atom -> Atom
+
+    A default feed is provided by :meth:`get_feed`. Subclasses that need
+    a customized feed should override :meth:`get_feed`.
+
+    """
+    FEED_TYPES = ['.atom', '.rss']
+    FEED_NAMES = ['feed{0}'.format(typ) for typ in FEED_TYPES]
+
+    def __getattr__(self, name):
+        if name in self.FEED_NAMES:
+            return self.feed
+        raise AttributeError(name)
+
+    def _get_feed_type(self, request):
+        for typ in self.FEED_TYPES:
+            if request.environ['PATH_INFO'].endswith(typ):
+                return typ.lstrip('.')
+        return 'rss'
+
+    @without_trailing_slash
+    @expose()
+    @validate(dict(
+            since=h.DateTimeConverter(if_empty=None, if_invalid=None),
+            until=h.DateTimeConverter(if_empty=None, if_invalid=None),
+            page=V.Int(if_empty=None),
+            limit=V.Int(if_empty=None)))
+    def feed(self, since=None, until=None, page=None, limit=None, **kw):
+        """Return a utf8-encoded XML feed (RSS or Atom) to the browser.
+        """
+        feed_def = self.get_feed(c.project, c.app, c.user)
+        if not feed_def:
+            raise exc.HTTPNotFound
+        feed = M.Feed.feed(
+            feed_def.query,
+            self._get_feed_type(request),
+            feed_def.title,
+            feed_def.url,
+            feed_def.description,
+            since, until, page, limit)
+        response.headers['Content-Type'] = ''
+        response.content_type = 'application/xml'
+        return feed.writeString('utf-8')
+
+    def get_feed(self, project, app, user):
+        """Return a default :class:`Feed` for this controller.
+
+        Subclasses should override to customize the feed.
+
+        :param project: :class:`allura.model.project.Project`
+        :param app: :class:`allura.app.Application`
+        :param user: :class:`allura.model.auth.User`
+        :rtype: :class:`Feed`
+
+        """
+        return Feed(
+            dict(project_id=project._id, app_config_id=app.config._id),
+            'Recent changes to %s' % app.config.options.mount_point,
+            app.url)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/Allura/allura/controllers/project.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/project.py b/Allura/allura/controllers/project.py
index 4f02632..3bfe0ca 100644
--- a/Allura/allura/controllers/project.py
+++ b/Allura/allura/controllers/project.py
@@ -40,6 +40,7 @@ from allura.lib import helpers as h
 from allura.lib import utils
 from allura.lib.decorators import require_post
 from allura.controllers.error import ErrorController
+from allura.controllers.feed import Feed, FeedController
 from allura.lib.security import require_access, has_access
 from allura.lib.security import RoleCache
 from allura.lib.widgets import forms as ff
@@ -299,11 +300,9 @@ class ToolListController(object):
                 if e.tool_name and e.tool_name.lower() == tool_name]
         return dict(entries=entries, type=entries[0].tool_name.capitalize() if entries else None)
 
-class ProjectController(object):
+class ProjectController(FeedController):
 
     def __init__(self):
-        setattr(self, 'feed.rss', self.feed)
-        setattr(self, 'feed.atom', self.feed)
         setattr(self, '_nav.json', self._nav)
         self.screenshot = ScreenshotsController()
         self._list = ToolListController()
@@ -367,29 +366,17 @@ class ProjectController(object):
         else:
             redirect(c.project.app_configs[0].options.mount_point + '/')
 
-    @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            until=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            page=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, page=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        title = 'Recent changes to Project %s' % c.project.name
-        feed = M.Feed.feed(
-            dict(project_id=c.project._id),
-            feed_type,
-            title,
-            c.project.url(),
-            title,
-            since, until, page, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
+    def get_feed(self, project, app, user):
+        """Return a :class:`allura.controllers.feed.Feed` object describing
+        the xml feed for this controller.
+
+        Overrides :meth:`allura.controllers.feed.FeedController.get_feed`.
+
+        """
+        return Feed(
+            dict(project_id=project._id),
+            'Recent changes to Project %s' % project.name,
+            project.url())
 
     @expose()
     def icon(self, **kw):

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/Allura/allura/controllers/repository.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/repository.py b/Allura/allura/controllers/repository.py
index 5e006ba..59b154e 100644
--- a/Allura/allura/controllers/repository.py
+++ b/Allura/allura/controllers/repository.py
@@ -49,6 +49,7 @@ from allura.lib.widgets.subscriptions import SubscribeForm
 from allura import model as M
 from allura.lib.widgets import form_fields as ffw
 from allura.controllers.base import DispatchIndex
+from allura.controllers.feed import FeedController
 from allura.lib.diff import HtmlSideBySideDiff
 from paste.deploy.converters import asbool
 from allura.app import SitemapEntry
@@ -61,14 +62,10 @@ def on_import():
     CommitBrowser.TreeBrowserClass = TreeBrowser
     TreeBrowser.FileBrowserClass = FileBrowser
 
-class RepoRootController(BaseController):
+class RepoRootController(BaseController, FeedController):
     _discuss = AppDiscussionController()
     commit_browser_widget=SCMCommitBrowserWidget()
 
-    def __init__(self):
-        setattr(self, 'feed.atom', self.feed)
-        setattr(self, 'feed.rss', self.feed)
-
     def _check_security(self):
         security.require(security.has_access(c.app, 'read'))
 
@@ -187,30 +184,6 @@ class RepoRootController(BaseController):
             redirect(mr.url())
 
     @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            until=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            offset=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, offset=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        title = 'Recent changes to %s' % c.app.config.options.mount_point
-        feed = M.Feed.feed(
-            dict(project_id=c.project._id,app_config_id=c.app.config._id),
-            feed_type,
-            title,
-            c.app.url,
-            title,
-            since, until, offset, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
-
-    @without_trailing_slash
     @expose('jinja:allura:templates/repo/commit_browser.html')
     def commit_browser(self):
         if not c.app.repo or c.app.repo.status != 'ready':

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/Allura/allura/ext/user_profile/user_main.py
----------------------------------------------------------------------
diff --git a/Allura/allura/ext/user_profile/user_main.py b/Allura/allura/ext/user_profile/user_main.py
index 0491199..c6e2402 100644
--- a/Allura/allura/ext/user_profile/user_main.py
+++ b/Allura/allura/ext/user_profile/user_main.py
@@ -32,6 +32,7 @@ from allura.lib.helpers import DateTimeConverter
 from allura.lib.security import require_access
 from allura.model import User, Feed, ACE
 from allura.controllers import BaseController
+from allura.controllers.feed import Feed, FeedController
 from allura.lib.decorators import require_post
 
 log = logging.getLogger(__name__)
@@ -79,11 +80,7 @@ class UserProfileApp(Application):
         pass
 
 
-class UserProfileController(BaseController):
-
-    def __init__(self):
-        setattr(self, 'feed.rss', self.feed)
-        setattr(self, 'feed.atom', self.feed)
+class UserProfileController(BaseController, FeedController):
 
     def _check_security(self):
         require_access(c.project, 'read')
@@ -94,38 +91,16 @@ class UserProfileController(BaseController):
         if not user:
             raise exc.HTTPNotFound()
         return dict(user=user)
-    # This will be fully implemented in a future iteration
-    # @expose('jinja:allura.ext.user_profile:templates/user_subscriptions.html')
-    # def subscriptions(self):
-    #     username = c.project.shortname.split('/')[1]
-    #     user = User.by_username(username)
-    #     subs = Subscriptions.query.find({'user_id':user._id}).all()
-    #     for sub in subs:
-    #         for s in sub.subscriptions:
-    #             r = g.solr_short_timeout.search(s.artifact_index_id)
-    #             print r.docs
-    #     return dict(user=user)
-
-    @expose()
-    @validate(dict(
-            since=DateTimeConverter(if_empty=None),
-            until=DateTimeConverter(if_empty=None),
-            page=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, page=None, limit=None, **kw):
-        user = c.project.user_project_of
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        title = 'Recent posts by %s' % user.display_name
-        feed = Feed.feed(
-            {'author_link':user.url()},
-            feed_type,
-            title,
-            c.project.url(),
-            title,
-            since, until, page, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
+
+    def get_feed(self, project, app, user):
+        """Return a :class:`allura.controllers.feed.Feed` object describing
+        the xml feed for this controller.
+
+        Overrides :meth:`allura.controllers.feed.FeedController.get_feed`.
+
+        """
+        user = project.user_project_of
+        return Feed(
+            {'author_link': user.url()},
+            'Recent posts by %s' % user.display_name,
+            project.url())

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/ForgeBlog/forgeblog/main.py
----------------------------------------------------------------------
diff --git a/ForgeBlog/forgeblog/main.py b/ForgeBlog/forgeblog/main.py
index f745af9..0c7af13 100644
--- a/ForgeBlog/forgeblog/main.py
+++ b/ForgeBlog/forgeblog/main.py
@@ -46,6 +46,7 @@ from allura.lib.widgets import form_fields as ffw
 from allura.lib.widgets.search import SearchResults, SearchHelp
 from allura import model as M
 from allura.controllers import BaseController, AppDiscussionController
+from allura.controllers.feed import Feed, FeedController
 
 # Local imports
 from forgeblog import model as BM
@@ -180,11 +181,9 @@ class ForgeBlogApp(Application):
         BM.BlogPostSnapshot.query.remove(dict(app_config_id=c.app.config._id))
         super(ForgeBlogApp, self).uninstall(project)
 
-class RootController(BaseController):
+class RootController(BaseController, FeedController):
 
     def __init__(self):
-        setattr(self, 'feed.atom', self.feed)
-        setattr(self, 'feed.rss', self.feed)
         self._discuss = AppDiscussionController()
 
     def _check_security(self):
@@ -255,30 +254,6 @@ class RootController(BaseController):
         redirect(h.really_unicode(post.url()).encode('utf-8'))
 
 
-    @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            until=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            offset=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, offset=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        title = '%s - %s' % (c.project.name, c.app.config.options.mount_label)
-        feed = M.Feed.feed(
-            dict(project_id=c.project._id, app_config_id=c.app.config._id),
-            feed_type,
-            title,
-            c.app.url,
-            title,
-            since, until, offset, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
-
     @with_trailing_slash
     @expose('jinja:allura:templates/markdown_syntax_dialog.html')
     def markdown_syntax_dialog(self, **kw):
@@ -293,12 +268,10 @@ class RootController(BaseController):
             raise exc.HTTPNotFound()
         return PostController(post), rest
 
-class PostController(BaseController):
+class PostController(BaseController, FeedController):
 
     def __init__(self, post):
         self.post = post
-        setattr(self, 'feed.atom', self.feed)
-        setattr(self, 'feed.rss', self.feed)
 
     def _check_security(self):
         require_access(self.post, 'read')
@@ -380,28 +353,17 @@ class PostController(BaseController):
             self.post.unsubscribe()
         redirect(h.really_unicode(request.referer).encode('utf-8'))
 
-    @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            until=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            offset=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, offset=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        feed = M.Feed.feed(
+    def get_feed(self, project, app, user):
+        """Return a :class:`allura.controllers.feed.Feed` object describing
+        the xml feed for this controller.
+
+        Overrides :meth:`allura.controllers.feed.FeedController.get_feed`.
+
+        """
+        return Feed(
             dict(ref_id=self.post.index_id()),
-            feed_type,
-            'Recent changes to %s' % self.post.title,
-            self.post.url(),
             'Recent changes to %s' % self.post.title,
-            since, until, offset, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
+            self.post.url())
 
     def _get_version(self, version):
         if not version: return self.post

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/ForgeDiscussion/forgediscussion/controllers/root.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index 02c3cde..f731da3 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -27,14 +27,13 @@ from pylons import request
 from formencode import validators
 from webob import exc
 
-
 from allura.lib.security import require_access, has_access, require_authenticated
-from allura.model import Feed
 from allura.lib.search import search_app
 from allura.lib import helpers as h
 from allura.lib.utils import AntiSpam
 from allura.lib.decorators import require_post
 from allura.controllers import BaseController, DispatchIndex
+from allura.controllers.feed import Feed, FeedController
 
 from .forum import ForumController
 from forgediscussion import import_support
@@ -48,7 +47,7 @@ from forgediscussion.widgets.admin import AddForumShort
 
 log = logging.getLogger(__name__)
 
-class RootController(BaseController, DispatchIndex):
+class RootController(BaseController, DispatchIndex, FeedController):
 
     class W(object):
         forum_subscription_form=FW.ForumSubscriptionForm()
@@ -58,10 +57,6 @@ class RootController(BaseController, DispatchIndex):
         search_results = SearchResults()
         search_help = SearchHelp(comments=False, history=False)
 
-    def __init__(self):
-        setattr(self, 'feed.rss', self.feed)
-        setattr(self, 'feed.atom', self.feed)
-
     def _check_security(self):
         require_access(c.app, 'read')
 
@@ -191,29 +186,18 @@ class RootController(BaseController, DispatchIndex):
                 obj['obj'].subscriptions.pop(str(c.user._id), None)
         redirect(request.referer)
 
-    @expose()
-    @validate(dict(
-            since=h.DateTimeConverter(if_empty=None),
-            until=h.DateTimeConverter(if_empty=None),
-            page=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, page=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        title = 'Recent posts to %s' % c.app.config.options.mount_label
-
-        feed = Feed.feed(
-            dict(project_id=c.project._id, app_config_id=c.app.config._id),
-            feed_type,
-            title,
-            c.app.url,
-            title,
-            since, until, page, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
+    def get_feed(self, project, app, user):
+        """Return a :class:`allura.controllers.feed.Feed` object describing
+        the xml feed for this controller.
+
+        Overrides :meth:`allura.controllers.feed.FeedController.get_feed`.
+
+        """
+        return Feed(
+            dict(project_id=project._id, app_config_id=app.config._id),
+             'Recent posts to %s' % app.config.options.mount_label,
+            app.url)
+
 
 class RootRestController(BaseController):
 

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/ForgeSVN/forgesvn/controllers.py
----------------------------------------------------------------------
diff --git a/ForgeSVN/forgesvn/controllers.py b/ForgeSVN/forgesvn/controllers.py
index 2e39fc7..66709f0 100644
--- a/ForgeSVN/forgesvn/controllers.py
+++ b/ForgeSVN/forgesvn/controllers.py
@@ -20,9 +20,10 @@ from tg.decorators import with_trailing_slash
 from pylons import tmpl_context as c
 
 from allura.controllers import repository
+from allura.controllers.feed import FeedController
 
 
-class BranchBrowser(repository.BranchBrowser):
+class BranchBrowser(repository.BranchBrowser, FeedController):
 
     def __init__(self):
         super(BranchBrowser, self).__init__(None)

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/ForgeSVN/forgesvn/svn_main.py
----------------------------------------------------------------------
diff --git a/ForgeSVN/forgesvn/svn_main.py b/ForgeSVN/forgesvn/svn_main.py
index ef7d008..dcb4fd6 100644
--- a/ForgeSVN/forgesvn/svn_main.py
+++ b/ForgeSVN/forgesvn/svn_main.py
@@ -64,9 +64,6 @@ class ForgeSVNApp(RepositoryApp):
         self.root = BranchBrowser()
         default_root = RepoRootController()
         self.root.refresh = default_root.refresh
-        self.root.feed = default_root.feed
-        setattr(self.root, 'feed.rss', self.root.feed)
-        setattr(self.root, 'feed.atom', self.root.feed)
         self.root.commit_browser = default_root.commit_browser
         self.root.commit_browser_data = default_root.commit_browser_data
         self.root.status = default_root.status

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/ForgeTracker/forgetracker/tracker_main.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index 349ef1e..6270e96 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -61,6 +61,7 @@ from allura.lib.zarkov_helpers import zero_fill_zarkov_result
 from allura.controllers import AppDiscussionController, AppDiscussionRestController
 from allura.controllers import attachments as ac
 from allura.controllers import BaseController
+from allura.controllers.feed import Feed, FeedController
 from allura.tasks import mail_tasks
 
 # Local imports
@@ -467,11 +468,9 @@ def solr_columns():
         columns.append(dict(name='votes', sort_name='votes_total_i', label='Votes', active=True))
     return columns
 
-class RootController(BaseController):
+class RootController(BaseController, FeedController):
 
     def __init__(self):
-        setattr(self, 'feed.atom', self.feed)
-        setattr(self, 'feed.rss', self.feed)
         setattr(self, 'search_feed.atom', self.search_feed)
         setattr(self, 'search_feed.rss', self.search_feed)
         self._discuss = AppDiscussionController()
@@ -711,30 +710,6 @@ class RootController(BaseController):
         'Static page explaining markdown.'
         return dict()
 
-    @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            until=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            offset=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, offset=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        title = 'Recent changes to %s' % c.app.config.options.mount_point
-        feed = M.Feed.feed(
-            dict(project_id=c.project._id,app_config_id=c.app.config._id),
-            feed_type,
-            title,
-            c.app.url,
-            title,
-            since, until, offset, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
-
     @expose()
     @h.vardec
     @require_post()
@@ -1233,7 +1208,7 @@ def filtered_by_subscription(tickets, project_id=None, app_config_id=None):
     return filtered
 
 
-class TicketController(BaseController):
+class TicketController(BaseController, FeedController):
 
     def __init__(self, ticket_num=None):
         if ticket_num is not None:
@@ -1248,8 +1223,6 @@ class TicketController(BaseController):
                     utils.permanent_redirect(self.ticket.url())
             self.attachment = AttachmentsController(self.ticket)
             # self.comments = CommentController(self.ticket)
-        setattr(self, 'feed.atom', self.feed)
-        setattr(self, 'feed.rss', self.feed)
 
     def _check_security(self):
         if self.ticket is not None:
@@ -1285,30 +1258,19 @@ class TicketController(BaseController):
         else:
             raise exc.HTTPNotFound, 'Ticket #%s does not exist.' % self.ticket_num
 
-    @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            until=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            offset=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, offset=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
+    def get_feed(self, project, app, user):
+        """Return a :class:`allura.controllers.feed.Feed` object describing
+        the xml feed for this controller.
+
+        Overrides :meth:`allura.controllers.feed.FeedController.get_feed`.
+
+        """
         title = 'Recent changes to %d: %s' % (
             self.ticket.ticket_num, self.ticket.summary)
-        feed = M.Feed.feed(
-            {'ref_id':self.ticket.index_id()},
-            feed_type,
-            title,
-            self.ticket.url(),
+        return Feed(
+            {'ref_id': self.ticket.index_id()},
             title,
-            since, until, offset, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
+            self.ticket.url())
 
     @expose()
     @require_post()

http://git-wip-us.apache.org/repos/asf/incubator-allura/blob/391aa922/ForgeWiki/forgewiki/wiki_main.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/wiki_main.py b/ForgeWiki/forgewiki/wiki_main.py
index b923350..dfb982b 100644
--- a/ForgeWiki/forgewiki/wiki_main.py
+++ b/ForgeWiki/forgewiki/wiki_main.py
@@ -41,6 +41,7 @@ from allura.lib.security import require_access, has_access
 from allura.controllers import AppDiscussionController, BaseController
 from allura.controllers import DispatchIndex
 from allura.controllers import attachments as ac
+from allura.controllers.feed import Feed, FeedController
 from allura.lib import widgets as w
 from allura.lib.widgets import form_fields as ffw
 from allura.lib.widgets.subscriptions import SubscribeForm
@@ -277,11 +278,9 @@ The wiki uses [Markdown](%s) syntax.
         WM.Globals.query.remove(dict(app_config_id=self.config._id))
         super(ForgeWikiApp, self).uninstall(project)
 
-class RootController(BaseController, DispatchIndex):
+class RootController(BaseController, DispatchIndex, FeedController):
 
     def __init__(self):
-        setattr(self, 'feed.atom', self.feed)
-        setattr(self, 'feed.rss', self.feed)
         c.create_page_lightbox = W.create_page_lightbox
         self._discuss = AppDiscussionController()
 
@@ -409,31 +408,8 @@ class RootController(BaseController, DispatchIndex):
         'Display a page about how to use markdown.'
         return dict(example=MARKDOWN_EXAMPLE)
 
-    @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            until=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            offset=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, offset=None, limit=None, **kw):
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        title = 'Recent changes to %s' % c.app.config.options.mount_point
-        feed = M.Feed.feed(
-            dict(project_id=c.project._id,app_config_id=c.app.config._id),
-            feed_type,
-            title,
-            c.app.url,
-            title,
-            since, until, offset, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
-
-class PageController(BaseController):
+
+class PageController(BaseController, FeedController):
 
     def __init__(self, title):
         self.title = h.really_unicode(unquote(title))
@@ -442,8 +418,6 @@ class PageController(BaseController):
         if self.page is not None:
             self.attachment = WikiAttachmentsController(self.page)
         c.create_page_lightbox = W.create_page_lightbox
-        setattr(self, 'feed.atom', self.feed)
-        setattr(self, 'feed.rss', self.feed)
 
     def _check_security(self):
         if self.page:
@@ -583,30 +557,19 @@ class PageController(BaseController):
             raise exc.HTTPNotFound
         return pformat(self.page)
 
-    @without_trailing_slash
-    @expose()
-    @validate(dict(
-            since=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            until=h.DateTimeConverter(if_empty=None, if_invalid=None),
-            offset=validators.Int(if_empty=None),
-            limit=validators.Int(if_empty=None)))
-    def feed(self, since=None, until=None, offset=None, limit=None, **kw):
+    def get_feed(self, project, app, user):
+        """Return a :class:`allura.controllers.feed.Feed` object describing
+        the xml feed for this controller.
+
+        Overrides :meth:`allura.controllers.feed.FeedController.get_feed`.
+
+        """
         if not self.page:
-            raise exc.HTTPNotFound
-        if request.environ['PATH_INFO'].endswith('.atom'):
-            feed_type = 'atom'
-        else:
-            feed_type = 'rss'
-        feed = M.Feed.feed(
-            {'ref_id':self.page.index_id()},
-            feed_type,
-            'Recent changes to %s' % self.page.title,
-            self.page.url(),
+            return None
+        return Feed(
+            {'ref_id': self.page.index_id()},
             'Recent changes to %s' % self.page.title,
-            since, until, offset, limit)
-        response.headers['Content-Type'] = ''
-        response.content_type = 'application/xml'
-        return feed.writeString('utf-8')
+            self.page.url())
 
     @without_trailing_slash
     @expose('json')