diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py --- a/rhodecode/apps/repository/__init__.py +++ b/rhodecode/apps/repository/__init__.py @@ -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, diff --git a/rhodecode/tests/functional/test_feed.py b/rhodecode/apps/repository/tests/test_repo_feed.py rename from rhodecode/tests/functional/test_feed.py rename to rhodecode/apps/repository/tests/test_repo_feed.py --- a/rhodecode/tests/functional/test_feed.py +++ b/rhodecode/apps/repository/tests/test_repo_feed.py @@ -17,59 +17,78 @@ # 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.model.db import User -from rhodecode.tests import * +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 TestFeedController(TestController): +class TestFeedView(TestController): - def test_rss(self, backend): + @pytest.mark.parametrize("feed_type,response_types,content_type",[ + ('rss', [''], + "application/rss+xml"), + ('atom', [''], + "application/atom+xml"), + ]) + def test_feed(self, backend, feed_type, response_types, content_type): self.log_user() - response = self.app.get(url(controller='feed', action='rss', - repo_name=backend.repo_name)) + 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 == "application/rss+xml" - assert """""" in response + assert response.content_type == content_type - def test_rss_with_auth_token(self, backend, user_admin): + @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( - url(controller='feed', action='rss', - repo_name=backend.repo_name, auth_token=auth_token, - status=200)) - assert response.content_type == "application/rss+xml" - assert """""" in response + response = self.app.get( + route_path( + '{}_feed_home'.format(feed_type), repo_name=backend.repo_name, + params=dict(auth_token=auth_token)), + status=200) - def test_rss_with_auth_token_of_wrong_type(self, backend, user_util): + 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( - url(controller='feed', action='rss', - repo_name=backend.repo_name, auth_token=auth_token), + 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( - url(controller='feed', action='rss', - repo_name=backend.repo_name, auth_token=auth_token), + route_path( + '{}_feed_home'.format(feed_type), repo_name=backend.repo_name, + params=dict(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 """""" in response - - tag1 = '' - tag2 = '' - assert tag1 in response or tag2 in response diff --git a/rhodecode/apps/repository/views/repo_feed.py b/rhodecode/apps/repository/views/repo_feed.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_feed.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- + +# 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 +# (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 . +# +# 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 pytz +import logging + +from beaker.cache import cache_region +from pyramid.view import view_config +from pyramid.response import Response +from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed + +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, + NotAnonymous, CSRFRequired) +from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer +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 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 + config = rhodecode.CONFIG + + return { + 'language': 'en-us', + 'feed_ttl': '5', # TTL of feed, + 'feed_include_diff': + str2bool(config.get('rss_include_diff', False)), + 'feed_items_per_page': + safe_int(config.get('rss_items_per_page', 20)), + 'feed_diff_limit': + # we need to protect from parsing huge diffs here other way + # we can kill the server + safe_int(config.get('rss_cut_off_limit', 32 * 1024)), + } + + 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') % (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): + diff_processor = DiffProcessor( + commit.diff(), diff_limit=self.feed_diff_limit) + _parsed = diff_processor.prepare(inline_diff=False) + limited_diff = isinstance(_parsed, LimitedDiffContainer) + + return _parsed, limited_diff + + def _get_title(self, commit): + return h.shorter(commit.message, 160) + + def _get_description(self, commit): + _renderer = self.request.get_partial_renderer( + 'feed/atom_feed_entry.mako') + parsed_diff, limited_diff = self._changes(commit) + return _renderer( + 'body', + commit=commit, + parsed_diff=parsed_diff, + limited_diff=limited_diff, + feed_include_diff=self.feed_include_diff, + ) + + def _set_timezone(self, date, tzinfo=pytz.utc): + if not getattr(date, "tzinfo", None): + date.replace(tzinfo=tzinfo) + return date + + def _get_commits(self): + 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') + @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 % 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 + ) + + for commit in reversed(self._get_commits()): + date = self._set_timezone(commit.date) + feed.add_item( + title=self._get_title(commit), + author_name=commit.author, + description=self._get_description(commit), + 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, 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 response + + @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @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 % 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 + ) + + for commit in reversed(self._get_commits()): + date = self._set_timezone(commit.date) + feed.add_item( + title=self._get_title(commit), + author_name=commit.author, + description=self._get_description(commit), + 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, 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 response diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -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 #========================================================================== diff --git a/rhodecode/controllers/feed.py b/rhodecode/controllers/feed.py deleted file mode 100644 --- a/rhodecode/controllers/feed.py +++ /dev/null @@ -1,179 +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 . -# -# 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/ - -""" -Feed controller for RhodeCode -""" - -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 rhodecode.model.db import CacheKey, UserApiKeys -from rhodecode.lib import helpers as h -from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator -from rhodecode.lib.base import BaseRepoController -from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer -from rhodecode.lib.utils2 import safe_int, str2bool -from rhodecode.lib.utils import PartialRenderer - -log = logging.getLogger(__name__) - - -class FeedController(BaseRepoController): - - def _get_config(self): - import rhodecode - config = rhodecode.CONFIG - - return { - 'language': 'en-us', - 'feed_ttl': '5', # TTL of feed, - 'feed_include_diff': - str2bool(config.get('rss_include_diff', False)), - 'feed_items_per_page': - safe_int(config.get('rss_items_per_page', 20)), - 'feed_diff_limit': - # we need to protect from parsing huge diffs here other way - # we can kill the server - safe_int(config.get('rss_cut_off_limit', 32 * 1024)), - } - - @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED]) - def __before__(self): - super(FeedController, self).__before__() - 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.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): - diff_processor = DiffProcessor( - commit.diff(), diff_limit=self.feed_diff_limit) - _parsed = diff_processor.prepare(inline_diff=False) - limited_diff = isinstance(_parsed, LimitedDiffContainer) - - return _parsed, limited_diff - - def _get_title(self, commit): - 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) - return _renderer( - 'body', - commit=commit, - parsed_diff=parsed_diff, - limited_diff=limited_diff, - feed_include_diff=self.feed_include_diff, - ) - - def _set_timezone(self, date, tzinfo=pytz.utc): - if not getattr(date, "tzinfo", None): - date.replace(tzinfo=tzinfo) - return date - - def _get_commits(self): - return list(c.rhodecode_repo[-self.feed_items_per_page:]) - - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') - def atom(self, repo_name): - """Produce an atom-1.0 feed via feedgenerator module""" - - @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, - language=self.language, - ttl=self.ttl - ) - - for commit in reversed(self._get_commits()): - date = self._set_timezone(commit.date) - feed.add_item( - 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), - pubdate=date,) - - return feed.mime_type, feed.writeString('utf-8') - - invalidator_context = CacheKey.repo_context_cache( - _generate_feed, repo_name, CacheKey.CACHE_TYPE_ATOM) - - with invalidator_context as context: - context.invalidate() - mime_type, feed = context.compute() - - response.content_type = mime_type - return feed - - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') - def rss(self, repo_name): - """Produce an rss2 feed via feedgenerator module""" - - @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, - language=self.language, - ttl=self.ttl - ) - - for commit in reversed(self._get_commits()): - date = self._set_timezone(commit.date) - feed.add_item( - 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), - pubdate=date,) - - return feed.mime_type, feed.writeString('utf-8') - - invalidator_context = CacheKey.repo_context_cache( - _generate_feed, repo_name, CacheKey.CACHE_TYPE_RSS) - - with invalidator_context as context: - context.invalidate() - mime_type, feed = context.compute() - - response.content_type = mime_type - return feed diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -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']); diff --git a/rhodecode/templates/data_table/_dt_elements.mako b/rhodecode/templates/data_table/_dt_elements.mako --- a/rhodecode/templates/data_table/_dt_elements.mako +++ b/rhodecode/templates/data_table/_dt_elements.mako @@ -92,17 +92,17 @@ <%def name="rss(name)"> %if c.rhodecode_user.username != h.DEFAULT_USER: - + %else: - + %endif <%def name="atom(name)"> %if c.rhodecode_user.username != h.DEFAULT_USER: - + %else: - + %endif diff --git a/rhodecode/templates/summary/base.mako b/rhodecode/templates/summary/base.mako --- a/rhodecode/templates/summary/base.mako +++ b/rhodecode/templates/summary/base.mako @@ -10,8 +10,8 @@ <%def name="head_extra()"> - - + + diff --git a/rhodecode/templates/summary/summary.mako b/rhodecode/templates/summary/summary.mako --- a/rhodecode/templates/summary/summary.mako +++ b/rhodecode/templates/summary/summary.mako @@ -14,9 +14,9 @@