repo-feed: moved from pylons controller to pyramid views.

This commit is contained in:
Daniel Dourvaris 2017-07-09 10:25:14 +02:00
parent ef1f17e46a
commit 81fa857318
9 changed files with 197 additions and 136 deletions

View file

@ -80,6 +80,21 @@ def includeme(config):
pattern='/{repo_name:.*?[^/]}/pull-request-data',
repo_route=True, repo_accepted_types=['hg', 'git'])
# commits aka changesets
# TODO(dan): handle default landing revision ?
config.add_route(
name='changeset_home',
pattern='/{repo_name:.*?[^/]}/changeset/{revision}',
repo_route=True)
config.add_route(
name='changeset_children',
pattern='/{repo_name:.*?[^/]}/changeset_children/{revision}',
repo_route=True)
config.add_route(
name='changeset_parents',
pattern='/{repo_name:.*?[^/]}/changeset_parents/{revision}',
repo_route=True)
# Settings
config.add_route(
name='edit_repo',
@ -143,6 +158,15 @@ def includeme(config):
name='strip_execute',
pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
# ATOM/RSS Feed
config.add_route(
name='rss_feed_home',
pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
config.add_route(
name='atom_feed_home',
pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
# NOTE(marcink): needs to be at the end for catch-all
add_route_with_slash(
config,

View file

@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
from rhodecode.model.auth_token import AuthTokenModel
from rhodecode.tests import TestController
def route_path(name, params=None, **kwargs):
import urllib
base_url = {
'rss_feed_home': '/{repo_name}/feed/rss',
'atom_feed_home': '/{repo_name}/feed/atom',
}[name].format(**kwargs)
if params:
base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
return base_url
class TestFeedView(TestController):
@pytest.mark.parametrize("feed_type,response_types,content_type",[
('rss', ['<rss version="2.0">'],
"application/rss+xml"),
('atom', ['<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">'],
"application/atom+xml"),
])
def test_feed(self, backend, feed_type, response_types, content_type):
self.log_user()
response = self.app.get(
route_path('{}_feed_home'.format(feed_type), repo_name=backend.repo_name))
for content in response_types:
assert content in response
assert response.content_type == content_type
@pytest.mark.parametrize("feed_type, content_type", [
('rss', "application/rss+xml"),
('atom', "application/atom+xml")
])
def test_feed_with_auth_token(
self, backend, user_admin, feed_type, content_type):
auth_token = user_admin.feed_token
assert auth_token != ''
response = self.app.get(
route_path(
'{}_feed_home'.format(feed_type), repo_name=backend.repo_name,
params=dict(auth_token=auth_token)),
status=200)
assert response.content_type == content_type
@pytest.mark.parametrize("feed_type", ['rss', 'atom'])
def test_feed_with_auth_token_of_wrong_type(
self, backend, user_util, feed_type):
user = user_util.create_user()
auth_token = AuthTokenModel().create(
user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
auth_token = auth_token.api_key
self.app.get(
route_path(
'{}_feed_home'.format(feed_type), repo_name=backend.repo_name,
params=dict(auth_token=auth_token)),
status=302)
auth_token = AuthTokenModel().create(
user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
auth_token = auth_token.api_key
self.app.get(
route_path(
'{}_feed_home'.format(feed_type), repo_name=backend.repo_name,
params=dict(auth_token=auth_token)),
status=200)

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
# Copyright (C) 2017-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
@ -18,31 +18,37 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
Feed controller for RhodeCode
"""
import pytz
import logging
import pytz
from pylons import url, response, tmpl_context as c
from pylons.i18n.translation import _
from beaker.cache import cache_region
from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
from pyramid.view import view_config
from pyramid.response import Response
from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
from rhodecode.model.db import CacheKey, UserApiKeys
from rhodecode.apps._base import RepoAppView
from rhodecode.lib import audit_logger
from rhodecode.lib import helpers as h
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from rhodecode.lib.base import BaseRepoController
from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
NotAnonymous, CSRFRequired)
from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
from rhodecode.lib.utils2 import safe_int, str2bool
from rhodecode.lib.utils import PartialRenderer
from rhodecode.lib.ext_json import json
from rhodecode.lib.utils2 import str2bool, safe_int
from rhodecode.model.db import UserApiKeys, CacheKey
log = logging.getLogger(__name__)
class FeedController(BaseRepoController):
class RepoFeedView(RepoAppView):
def load_default_context(self):
c = self._get_local_tmpl_context()
# TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
c.repo_info = self.db_repo
self._register_global_c(c)
self._load_defaults()
return c
def _get_config(self):
import rhodecode
@ -61,20 +67,19 @@ class FeedController(BaseRepoController):
safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
}
@LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
def __before__(self):
super(FeedController, self).__before__()
def _load_defaults(self):
_ = self.request.translate
config = self._get_config()
# common values for feeds
self.description = _('Changes on %s repository')
self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
self.language = config["language"]
self.ttl = config["feed_ttl"]
self.feed_include_diff = config['feed_include_diff']
self.feed_diff_limit = config['feed_diff_limit']
self.feed_items_per_page = config['feed_items_per_page']
def __changes(self, commit):
def _changes(self, commit):
diff_processor = DiffProcessor(
commit.diff(), diff_limit=self.feed_diff_limit)
_parsed = diff_processor.prepare(inline_diff=False)
@ -86,8 +91,9 @@ class FeedController(BaseRepoController):
return h.shorter(commit.message, 160)
def _get_description(self, commit):
_renderer = PartialRenderer('feed/atom_feed_entry.mako')
parsed_diff, limited_diff = self.__changes(commit)
_renderer = self.request.get_partial_renderer(
'feed/atom_feed_entry.mako')
parsed_diff, limited_diff = self._changes(commit)
return _renderer(
'body',
commit=commit,
@ -102,19 +108,26 @@ class FeedController(BaseRepoController):
return date
def _get_commits(self):
return list(c.rhodecode_repo[-self.feed_items_per_page:])
return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
@LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def atom(self, repo_name):
"""Produce an atom-1.0 feed via feedgenerator module"""
@view_config(
route_name='atom_feed_home', request_method='GET',
renderer=None)
def atom(self):
"""
Produce an atom-1.0 feed via feedgenerator module
"""
self.load_default_context()
@cache_region('long_term')
def _generate_feed(cache_key):
feed = Atom1Feed(
title=self.title % repo_name,
link=h.route_url('repo_summary', repo_name=repo_name),
description=self.description % repo_name,
title=self.title % self.db_repo_name,
link=h.route_url('repo_summary', repo_name=self.db_repo_name),
description=self.description % self.db_repo_name,
language=self.language,
ttl=self.ttl
)
@ -125,33 +138,42 @@ class FeedController(BaseRepoController):
title=self._get_title(commit),
author_name=commit.author,
description=self._get_description(commit),
link=url('changeset_home', repo_name=repo_name,
revision=commit.raw_id, qualified=True),
link=h.route_url(
'changeset_home', repo_name=self.db_repo_name,
revision=commit.raw_id),
pubdate=date,)
return feed.mime_type, feed.writeString('utf-8')
invalidator_context = CacheKey.repo_context_cache(
_generate_feed, repo_name, CacheKey.CACHE_TYPE_ATOM)
_generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
with invalidator_context as context:
context.invalidate()
mime_type, feed = context.compute()
response = Response(feed)
response.content_type = mime_type
return feed
return response
@LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def rss(self, repo_name):
"""Produce an rss2 feed via feedgenerator module"""
@view_config(
route_name='rss_feed_home', request_method='GET',
renderer=None)
def rss(self):
"""
Produce an rss2 feed via feedgenerator module
"""
self.load_default_context()
@cache_region('long_term')
def _generate_feed(cache_key):
feed = Rss201rev2Feed(
title=self.title % repo_name,
link=h.route_url('repo_summary', repo_name=repo_name),
description=self.description % repo_name,
title=self.title % self.db_repo_name,
link=h.route_url('repo_summary', repo_name=self.db_repo_name),
description=self.description % self.db_repo_name,
language=self.language,
ttl=self.ttl
)
@ -162,18 +184,20 @@ class FeedController(BaseRepoController):
title=self._get_title(commit),
author_name=commit.author,
description=self._get_description(commit),
link=url('changeset_home', repo_name=repo_name,
revision=commit.raw_id, qualified=True),
link=h.route_url(
'changeset_home', repo_name=self.db_repo_name,
revision=commit.raw_id),
pubdate=date,)
return feed.mime_type, feed.writeString('utf-8')
invalidator_context = CacheKey.repo_context_cache(
_generate_feed, repo_name, CacheKey.CACHE_TYPE_RSS)
_generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
with invalidator_context as context:
context.invalidate()
mime_type, feed = context.compute()
response = Response(feed)
response.content_type = mime_type
return feed
return response

View file

@ -510,17 +510,6 @@ def make_map(config):
controller='journal', action='toggle_following', jsroute=True,
conditions={'method': ['POST']})
# FEEDS
rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
controller='feed', action='rss',
conditions={'function': check_repo},
requirements=URL_NAME_REQUIREMENTS)
rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
controller='feed', action='atom',
conditions={'function': check_repo},
requirements=URL_NAME_REQUIREMENTS)
#==========================================================================
# REPOSITORY ROUTES
#==========================================================================

View file

@ -113,6 +113,9 @@ function registerRCRoutes() {
pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
pyroutes.register('changeset_children', '/%(repo_name)s/changeset_children/%(revision)s', ['repo_name', 'revision']);
pyroutes.register('changeset_parents', '/%(repo_name)s/changeset_parents/%(revision)s', ['repo_name', 'revision']);
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
@ -128,6 +131,8 @@ function registerRCRoutes() {
pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);

View file

@ -92,17 +92,17 @@
<%def name="rss(name)">
%if c.rhodecode_user.username != h.DEFAULT_USER:
<a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.url('rss_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
<a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
%else:
<a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.url('rss_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
<a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
%endif
</%def>
<%def name="atom(name)">
%if c.rhodecode_user.username != h.DEFAULT_USER:
<a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.url('atom_feed_home',repo_name=name,auth_token=c.rhodecode_user.feed_token)}"><i class="icon-rss-sign"></i></a>
<a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
%else:
<a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.url('atom_feed_home',repo_name=name)}"><i class="icon-rss-sign"></i></a>
<a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
%endif
</%def>

View file

@ -10,8 +10,8 @@
<%def name="head_extra()">
<link href="${h.url('atom_feed_home',repo_name=c.rhodecode_db_repo.repo_name,auth_token=c.rhodecode_user.feed_token)}" rel="alternate" title="${h.tooltip(_('%s ATOM feed') % c.repo_name)}" type="application/atom+xml" />
<link href="${h.url('rss_feed_home',repo_name=c.rhodecode_db_repo.repo_name,auth_token=c.rhodecode_user.feed_token)}" rel="alternate" title="${h.tooltip(_('%s RSS feed') % c.repo_name)}" type="application/rss+xml" />
<link href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name, _query=dict(auth_token=c.rhodecode_user.feed_token))}" rel="alternate" title="${h.tooltip(_('%s ATOM feed') % c.repo_name)}" type="application/atom+xml" />
<link href="${h.route_path('rss_feed_home', repo_name=c.rhodecode_db_repo.repo_name, _query=dict(auth_token=c.rhodecode_user.feed_token))}" rel="alternate" title="${h.tooltip(_('%s RSS feed') % c.repo_name)}" type="application/rss+xml" />
</%def>

View file

@ -14,9 +14,9 @@
<ul class="links icon-only-links block-right">
<li>
%if c.rhodecode_user.username != h.DEFAULT_USER:
<a href="${h.url('atom_feed_home',repo_name=c.rhodecode_db_repo.repo_name,auth_token=c.rhodecode_user.feed_token)}" title="${_('RSS Feed')}"><i class="icon-rss-sign"></i></a>
<a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}"><i class="icon-rss-sign"></i></a>
%else:
<a href="${h.url('atom_feed_home',repo_name=c.rhodecode_db_repo.repo_name)}" title="${_('RSS Feed')}"><i class="icon-rss-sign"></i></a>
<a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_name)}" title="${_('RSS Feed')}"><i class="icon-rss-sign"></i></a>
%endif
</li>
</ul>

View file

@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2017 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
from rhodecode.model.auth_token import AuthTokenModel
from rhodecode.model.db import User
from rhodecode.tests import *
class TestFeedController(TestController):
def test_rss(self, backend):
self.log_user()
response = self.app.get(url(controller='feed', action='rss',
repo_name=backend.repo_name))
assert response.content_type == "application/rss+xml"
assert """<rss version="2.0">""" in response
def test_rss_with_auth_token(self, backend, user_admin):
auth_token = user_admin.feed_token
assert auth_token != ''
response = self.app.get(
url(controller='feed', action='rss',
repo_name=backend.repo_name, auth_token=auth_token,
status=200))
assert response.content_type == "application/rss+xml"
assert """<rss version="2.0">""" in response
def test_rss_with_auth_token_of_wrong_type(self, backend, user_util):
user = user_util.create_user()
auth_token = AuthTokenModel().create(
user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
auth_token = auth_token.api_key
self.app.get(
url(controller='feed', action='rss',
repo_name=backend.repo_name, auth_token=auth_token),
status=302)
auth_token = AuthTokenModel().create(
user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
auth_token = auth_token.api_key
self.app.get(
url(controller='feed', action='rss',
repo_name=backend.repo_name, auth_token=auth_token),
status=200)
def test_atom(self, backend):
self.log_user()
response = self.app.get(url(controller='feed', action='atom',
repo_name=backend.repo_name))
assert response.content_type == """application/atom+xml"""
assert """<?xml version="1.0" encoding="utf-8"?>""" in response
tag1 = '<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">'
tag2 = '<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom">'
assert tag1 in response or tag2 in response