##// END OF EJS Templates
repo-feed: moved from pylons controller to pyramid views.
dan -
r1899:4c10034c default
parent child Browse files
Show More
@@ -0,0 +1,203 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytz
22 import logging
23
24 from beaker.cache import cache_region
25 from pyramid.view import view_config
26 from pyramid.response import Response
27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
28
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
33 NotAnonymous, CSRFRequired)
34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.utils2 import str2bool, safe_int
37 from rhodecode.model.db import UserApiKeys, CacheKey
38
39 log = logging.getLogger(__name__)
40
41
42 class RepoFeedView(RepoAppView):
43 def load_default_context(self):
44 c = self._get_local_tmpl_context()
45
46 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
47 c.repo_info = self.db_repo
48
49 self._register_global_c(c)
50 self._load_defaults()
51 return c
52
53 def _get_config(self):
54 import rhodecode
55 config = rhodecode.CONFIG
56
57 return {
58 'language': 'en-us',
59 'feed_ttl': '5', # TTL of feed,
60 'feed_include_diff':
61 str2bool(config.get('rss_include_diff', False)),
62 'feed_items_per_page':
63 safe_int(config.get('rss_items_per_page', 20)),
64 'feed_diff_limit':
65 # we need to protect from parsing huge diffs here other way
66 # we can kill the server
67 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
68 }
69
70 def _load_defaults(self):
71 _ = self.request.translate
72 config = self._get_config()
73 # common values for feeds
74 self.description = _('Changes on %s repository')
75 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
76 self.language = config["language"]
77 self.ttl = config["feed_ttl"]
78 self.feed_include_diff = config['feed_include_diff']
79 self.feed_diff_limit = config['feed_diff_limit']
80 self.feed_items_per_page = config['feed_items_per_page']
81
82 def _changes(self, commit):
83 diff_processor = DiffProcessor(
84 commit.diff(), diff_limit=self.feed_diff_limit)
85 _parsed = diff_processor.prepare(inline_diff=False)
86 limited_diff = isinstance(_parsed, LimitedDiffContainer)
87
88 return _parsed, limited_diff
89
90 def _get_title(self, commit):
91 return h.shorter(commit.message, 160)
92
93 def _get_description(self, commit):
94 _renderer = self.request.get_partial_renderer(
95 'feed/atom_feed_entry.mako')
96 parsed_diff, limited_diff = self._changes(commit)
97 return _renderer(
98 'body',
99 commit=commit,
100 parsed_diff=parsed_diff,
101 limited_diff=limited_diff,
102 feed_include_diff=self.feed_include_diff,
103 )
104
105 def _set_timezone(self, date, tzinfo=pytz.utc):
106 if not getattr(date, "tzinfo", None):
107 date.replace(tzinfo=tzinfo)
108 return date
109
110 def _get_commits(self):
111 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
112
113 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
114 @HasRepoPermissionAnyDecorator(
115 'repository.read', 'repository.write', 'repository.admin')
116 @view_config(
117 route_name='atom_feed_home', request_method='GET',
118 renderer=None)
119 def atom(self):
120 """
121 Produce an atom-1.0 feed via feedgenerator module
122 """
123 self.load_default_context()
124
125 @cache_region('long_term')
126 def _generate_feed(cache_key):
127 feed = Atom1Feed(
128 title=self.title % self.db_repo_name,
129 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
130 description=self.description % self.db_repo_name,
131 language=self.language,
132 ttl=self.ttl
133 )
134
135 for commit in reversed(self._get_commits()):
136 date = self._set_timezone(commit.date)
137 feed.add_item(
138 title=self._get_title(commit),
139 author_name=commit.author,
140 description=self._get_description(commit),
141 link=h.route_url(
142 'changeset_home', repo_name=self.db_repo_name,
143 revision=commit.raw_id),
144 pubdate=date,)
145
146 return feed.mime_type, feed.writeString('utf-8')
147
148 invalidator_context = CacheKey.repo_context_cache(
149 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
150
151 with invalidator_context as context:
152 context.invalidate()
153 mime_type, feed = context.compute()
154
155 response = Response(feed)
156 response.content_type = mime_type
157 return response
158
159 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
160 @HasRepoPermissionAnyDecorator(
161 'repository.read', 'repository.write', 'repository.admin')
162 @view_config(
163 route_name='rss_feed_home', request_method='GET',
164 renderer=None)
165 def rss(self):
166 """
167 Produce an rss2 feed via feedgenerator module
168 """
169 self.load_default_context()
170
171 @cache_region('long_term')
172 def _generate_feed(cache_key):
173 feed = Rss201rev2Feed(
174 title=self.title % self.db_repo_name,
175 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
176 description=self.description % self.db_repo_name,
177 language=self.language,
178 ttl=self.ttl
179 )
180
181 for commit in reversed(self._get_commits()):
182 date = self._set_timezone(commit.date)
183 feed.add_item(
184 title=self._get_title(commit),
185 author_name=commit.author,
186 description=self._get_description(commit),
187 link=h.route_url(
188 'changeset_home', repo_name=self.db_repo_name,
189 revision=commit.raw_id),
190 pubdate=date,)
191
192 return feed.mime_type, feed.writeString('utf-8')
193
194 invalidator_context = CacheKey.repo_context_cache(
195 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
196
197 with invalidator_context as context:
198 context.invalidate()
199 mime_type, feed = context.compute()
200
201 response = Response(feed)
202 response.content_type = mime_type
203 return response
@@ -80,6 +80,21 b' def includeme(config):'
80 80 pattern='/{repo_name:.*?[^/]}/pull-request-data',
81 81 repo_route=True, repo_accepted_types=['hg', 'git'])
82 82
83 # commits aka changesets
84 # TODO(dan): handle default landing revision ?
85 config.add_route(
86 name='changeset_home',
87 pattern='/{repo_name:.*?[^/]}/changeset/{revision}',
88 repo_route=True)
89 config.add_route(
90 name='changeset_children',
91 pattern='/{repo_name:.*?[^/]}/changeset_children/{revision}',
92 repo_route=True)
93 config.add_route(
94 name='changeset_parents',
95 pattern='/{repo_name:.*?[^/]}/changeset_parents/{revision}',
96 repo_route=True)
97
83 98 # Settings
84 99 config.add_route(
85 100 name='edit_repo',
@@ -143,6 +158,15 b' def includeme(config):'
143 158 name='strip_execute',
144 159 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
145 160
161 # ATOM/RSS Feed
162 config.add_route(
163 name='rss_feed_home',
164 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
165
166 config.add_route(
167 name='atom_feed_home',
168 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
169
146 170 # NOTE(marcink): needs to be at the end for catch-all
147 171 add_route_with_slash(
148 172 config,
@@ -17,59 +17,78 b''
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import pytest
20 21 from rhodecode.model.auth_token import AuthTokenModel
21 from rhodecode.model.db import User
22 from rhodecode.tests import *
22 from rhodecode.tests import TestController
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib
27
28 base_url = {
29 'rss_feed_home': '/{repo_name}/feed/rss',
30 'atom_feed_home': '/{repo_name}/feed/atom',
31 }[name].format(**kwargs)
32
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 return base_url
23 36
24 37
25 class TestFeedController(TestController):
38 class TestFeedView(TestController):
26 39
27 def test_rss(self, backend):
40 @pytest.mark.parametrize("feed_type,response_types,content_type",[
41 ('rss', ['<rss version="2.0">'],
42 "application/rss+xml"),
43 ('atom', ['<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">'],
44 "application/atom+xml"),
45 ])
46 def test_feed(self, backend, feed_type, response_types, content_type):
28 47 self.log_user()
29 response = self.app.get(url(controller='feed', action='rss',
30 repo_name=backend.repo_name))
48 response = self.app.get(
49 route_path('{}_feed_home'.format(feed_type), repo_name=backend.repo_name))
50
51 for content in response_types:
52 assert content in response
31 53
32 assert response.content_type == "application/rss+xml"
33 assert """<rss version="2.0">""" in response
54 assert response.content_type == content_type
34 55
35 def test_rss_with_auth_token(self, backend, user_admin):
56 @pytest.mark.parametrize("feed_type, content_type", [
57 ('rss', "application/rss+xml"),
58 ('atom', "application/atom+xml")
59 ])
60 def test_feed_with_auth_token(
61 self, backend, user_admin, feed_type, content_type):
36 62 auth_token = user_admin.feed_token
37 63 assert auth_token != ''
38 response = self.app.get(
39 url(controller='feed', action='rss',
40 repo_name=backend.repo_name, auth_token=auth_token,
41 status=200))
42 64
43 assert response.content_type == "application/rss+xml"
44 assert """<rss version="2.0">""" in response
65 response = self.app.get(
66 route_path(
67 '{}_feed_home'.format(feed_type), repo_name=backend.repo_name,
68 params=dict(auth_token=auth_token)),
69 status=200)
45 70
46 def test_rss_with_auth_token_of_wrong_type(self, backend, user_util):
71 assert response.content_type == content_type
72
73 @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
74 def test_feed_with_auth_token_of_wrong_type(
75 self, backend, user_util, feed_type):
47 76 user = user_util.create_user()
48 77 auth_token = AuthTokenModel().create(
49 78 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
50 79 auth_token = auth_token.api_key
51 80
52 81 self.app.get(
53 url(controller='feed', action='rss',
54 repo_name=backend.repo_name, auth_token=auth_token),
82 route_path(
83 '{}_feed_home'.format(feed_type), repo_name=backend.repo_name,
84 params=dict(auth_token=auth_token)),
55 85 status=302)
56 86
57 87 auth_token = AuthTokenModel().create(
58 88 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
59 89 auth_token = auth_token.api_key
60 90 self.app.get(
61 url(controller='feed', action='rss',
62 repo_name=backend.repo_name, auth_token=auth_token),
91 route_path(
92 '{}_feed_home'.format(feed_type), repo_name=backend.repo_name,
93 params=dict(auth_token=auth_token)),
63 94 status=200)
64
65 def test_atom(self, backend):
66 self.log_user()
67 response = self.app.get(url(controller='feed', action='atom',
68 repo_name=backend.repo_name))
69
70 assert response.content_type == """application/atom+xml"""
71 assert """<?xml version="1.0" encoding="utf-8"?>""" in response
72
73 tag1 = '<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">'
74 tag2 = '<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom">'
75 assert tag1 in response or tag2 in response
@@ -510,17 +510,6 b' def make_map(config):'
510 510 controller='journal', action='toggle_following', jsroute=True,
511 511 conditions={'method': ['POST']})
512 512
513 # FEEDS
514 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
515 controller='feed', action='rss',
516 conditions={'function': check_repo},
517 requirements=URL_NAME_REQUIREMENTS)
518
519 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
520 controller='feed', action='atom',
521 conditions={'function': check_repo},
522 requirements=URL_NAME_REQUIREMENTS)
523
524 513 #==========================================================================
525 514 # REPOSITORY ROUTES
526 515 #==========================================================================
@@ -113,6 +113,9 b' function registerRCRoutes() {'
113 113 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
114 114 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
115 115 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
116 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
117 pyroutes.register('changeset_children', '/%(repo_name)s/changeset_children/%(revision)s', ['repo_name', 'revision']);
118 pyroutes.register('changeset_parents', '/%(repo_name)s/changeset_parents/%(revision)s', ['repo_name', 'revision']);
116 119 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
117 120 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
118 121 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
@@ -128,6 +131,8 b' function registerRCRoutes() {'
128 131 pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']);
129 132 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
130 133 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
134 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed/rss', ['repo_name']);
135 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed/atom', ['repo_name']);
131 136 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
132 137 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
133 138 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
@@ -92,17 +92,17 b''
92 92
93 93 <%def name="rss(name)">
94 94 %if c.rhodecode_user.username != h.DEFAULT_USER:
95 <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>
95 <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>
96 96 %else:
97 <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>
97 <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>
98 98 %endif
99 99 </%def>
100 100
101 101 <%def name="atom(name)">
102 102 %if c.rhodecode_user.username != h.DEFAULT_USER:
103 <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>
103 <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>
104 104 %else:
105 <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>
105 <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>
106 106 %endif
107 107 </%def>
108 108
@@ -10,8 +10,8 b''
10 10
11 11
12 12 <%def name="head_extra()">
13 <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" />
14 <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" />
13 <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" />
14 <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" />
15 15 </%def>
16 16
17 17
@@ -14,9 +14,9 b''
14 14 <ul class="links icon-only-links block-right">
15 15 <li>
16 16 %if c.rhodecode_user.username != h.DEFAULT_USER:
17 <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>
17 <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>
18 18 %else:
19 <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>
19 <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>
20 20 %endif
21 21 </li>
22 22 </ul>
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now