##// END OF EJS Templates
feeds: generate entries with proper unique ids....
ergo -
r2071:263f1c18 default
parent child Browse files
Show More
@@ -1,377 +1,384 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import itertools
23 import itertools
24
24
25 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
25 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
26
26
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.httpexceptions import HTTPBadRequest
28 from pyramid.httpexceptions import HTTPBadRequest
29 from pyramid.response import Response
29 from pyramid.response import Response
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import BaseAppView
32 from rhodecode.apps._base import BaseAppView
33 from rhodecode.model.db import (
33 from rhodecode.model.db import (
34 or_, joinedload, UserLog, UserFollowing, User, UserApiKeys)
34 or_, joinedload, UserLog, UserFollowing, User, UserApiKeys)
35 from rhodecode.model.meta import Session
35 from rhodecode.model.meta import Session
36 import rhodecode.lib.helpers as h
36 import rhodecode.lib.helpers as h
37 from rhodecode.lib.helpers import Page
37 from rhodecode.lib.helpers import Page
38 from rhodecode.lib.user_log_filter import user_log_filter
38 from rhodecode.lib.user_log_filter import user_log_filter
39 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
39 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
40 from rhodecode.lib.utils2 import safe_int, AttributeDict
40 from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe
41 from rhodecode.model.scm import ScmModel
41 from rhodecode.model.scm import ScmModel
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class JournalView(BaseAppView):
46 class JournalView(BaseAppView):
47
47
48 def load_default_context(self):
48 def load_default_context(self):
49 c = self._get_local_tmpl_context(include_app_defaults=True)
49 c = self._get_local_tmpl_context(include_app_defaults=True)
50 self._register_global_c(c)
50 self._register_global_c(c)
51 self._load_defaults(c.rhodecode_name)
51 self._load_defaults(c.rhodecode_name)
52
52
53 # TODO(marcink): what is this, why we need a global register ?
53 # TODO(marcink): what is this, why we need a global register ?
54 c.search_term = self.request.GET.get('filter') or ''
54 c.search_term = self.request.GET.get('filter') or ''
55 return c
55 return c
56
56
57 def _get_config(self, rhodecode_name):
57 def _get_config(self, rhodecode_name):
58 import rhodecode
58 import rhodecode
59 config = rhodecode.CONFIG
59 config = rhodecode.CONFIG
60
60
61 return {
61 return {
62 'language': 'en-us',
62 'language': 'en-us',
63 'feed_ttl': '5', # TTL of feed,
63 'feed_ttl': '5', # TTL of feed,
64 'feed_items_per_page':
64 'feed_items_per_page':
65 safe_int(config.get('rss_items_per_page', 20)),
65 safe_int(config.get('rss_items_per_page', 20)),
66 'rhodecode_name': rhodecode_name
66 'rhodecode_name': rhodecode_name
67 }
67 }
68
68
69 def _load_defaults(self, rhodecode_name):
69 def _load_defaults(self, rhodecode_name):
70 config = self._get_config(rhodecode_name)
70 config = self._get_config(rhodecode_name)
71 # common values for feeds
71 # common values for feeds
72 self.language = config["language"]
72 self.language = config["language"]
73 self.ttl = config["feed_ttl"]
73 self.ttl = config["feed_ttl"]
74 self.feed_items_per_page = config['feed_items_per_page']
74 self.feed_items_per_page = config['feed_items_per_page']
75 self.rhodecode_name = config['rhodecode_name']
75 self.rhodecode_name = config['rhodecode_name']
76
76
77 def _get_daily_aggregate(self, journal):
77 def _get_daily_aggregate(self, journal):
78 groups = []
78 groups = []
79 for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
79 for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
80 user_group = []
80 user_group = []
81 # groupby username if it's a present value, else
81 # groupby username if it's a present value, else
82 # fallback to journal username
82 # fallback to journal username
83 for _, g2 in itertools.groupby(
83 for _, g2 in itertools.groupby(
84 list(g), lambda x: x.user.username if x.user else x.username):
84 list(g), lambda x: x.user.username if x.user else x.username):
85 l = list(g2)
85 l = list(g2)
86 user_group.append((l[0].user, l))
86 user_group.append((l[0].user, l))
87
87
88 groups.append((k, user_group,))
88 groups.append((k, user_group,))
89
89
90 return groups
90 return groups
91
91
92 def _get_journal_data(self, following_repos, search_term):
92 def _get_journal_data(self, following_repos, search_term):
93 repo_ids = [x.follows_repository.repo_id for x in following_repos
93 repo_ids = [x.follows_repository.repo_id for x in following_repos
94 if x.follows_repository is not None]
94 if x.follows_repository is not None]
95 user_ids = [x.follows_user.user_id for x in following_repos
95 user_ids = [x.follows_user.user_id for x in following_repos
96 if x.follows_user is not None]
96 if x.follows_user is not None]
97
97
98 filtering_criterion = None
98 filtering_criterion = None
99
99
100 if repo_ids and user_ids:
100 if repo_ids and user_ids:
101 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
101 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
102 UserLog.user_id.in_(user_ids))
102 UserLog.user_id.in_(user_ids))
103 if repo_ids and not user_ids:
103 if repo_ids and not user_ids:
104 filtering_criterion = UserLog.repository_id.in_(repo_ids)
104 filtering_criterion = UserLog.repository_id.in_(repo_ids)
105 if not repo_ids and user_ids:
105 if not repo_ids and user_ids:
106 filtering_criterion = UserLog.user_id.in_(user_ids)
106 filtering_criterion = UserLog.user_id.in_(user_ids)
107 if filtering_criterion is not None:
107 if filtering_criterion is not None:
108 journal = Session().query(UserLog)\
108 journal = Session().query(UserLog)\
109 .options(joinedload(UserLog.user))\
109 .options(joinedload(UserLog.user))\
110 .options(joinedload(UserLog.repository))
110 .options(joinedload(UserLog.repository))
111 # filter
111 # filter
112 try:
112 try:
113 journal = user_log_filter(journal, search_term)
113 journal = user_log_filter(journal, search_term)
114 except Exception:
114 except Exception:
115 # we want this to crash for now
115 # we want this to crash for now
116 raise
116 raise
117 journal = journal.filter(filtering_criterion)\
117 journal = journal.filter(filtering_criterion)\
118 .order_by(UserLog.action_date.desc())
118 .order_by(UserLog.action_date.desc())
119 else:
119 else:
120 journal = []
120 journal = []
121
121
122 return journal
122 return journal
123
123
124 def feed_uid(self, entry_id):
125 return '{}:{}'.format('journal', md5_safe(entry_id))
126
124 def _atom_feed(self, repos, search_term, public=True):
127 def _atom_feed(self, repos, search_term, public=True):
125 _ = self.request.translate
128 _ = self.request.translate
126 journal = self._get_journal_data(repos, search_term)
129 journal = self._get_journal_data(repos, search_term)
127 if public:
130 if public:
128 _link = h.route_url('journal_public_atom')
131 _link = h.route_url('journal_public_atom')
129 _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
132 _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
130 'atom feed')
133 'atom feed')
131 else:
134 else:
132 _link = h.route_url('journal_atom')
135 _link = h.route_url('journal_atom')
133 _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
136 _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
134
137
135 feed = Atom1Feed(
138 feed = Atom1Feed(
136 title=_desc, link=_link, description=_desc,
139 title=_desc, link=_link, description=_desc,
137 language=self.language, ttl=self.ttl)
140 language=self.language, ttl=self.ttl)
138
141
139 for entry in journal[:self.feed_items_per_page]:
142 for entry in journal[:self.feed_items_per_page]:
140 user = entry.user
143 user = entry.user
141 if user is None:
144 if user is None:
142 # fix deleted users
145 # fix deleted users
143 user = AttributeDict({'short_contact': entry.username,
146 user = AttributeDict({'short_contact': entry.username,
144 'email': '',
147 'email': '',
145 'full_contact': ''})
148 'full_contact': ''})
146 action, action_extra, ico = h.action_parser(entry, feed=True)
149 action, action_extra, ico = h.action_parser(entry, feed=True)
147 title = "%s - %s %s" % (user.short_contact, action(),
150 title = "%s - %s %s" % (user.short_contact, action(),
148 entry.repository.repo_name)
151 entry.repository.repo_name)
149 desc = action_extra()
152 desc = action_extra()
150 _url = h.route_url('home')
153 _url = h.route_url('home')
151 if entry.repository is not None:
154 if entry.repository is not None:
152 _url = h.route_url('repo_changelog',
155 _url = h.route_url('repo_changelog',
153 repo_name=entry.repository.repo_name)
156 repo_name=entry.repository.repo_name)
154
157
155 feed.add_item(title=title,
158 feed.add_item(
156 pubdate=entry.action_date,
159 unique_id=self.feed_uid(entry.user_log_id),
157 link=_url,
160 title=title,
158 author_email=user.email,
161 pubdate=entry.action_date,
159 author_name=user.full_contact,
162 link=_url,
160 description=desc)
163 author_email=user.email,
164 author_name=user.full_contact,
165 description=desc)
161
166
162 response = Response(feed.writeString('utf-8'))
167 response = Response(feed.writeString('utf-8'))
163 response.content_type = feed.mime_type
168 response.content_type = feed.mime_type
164 return response
169 return response
165
170
166 def _rss_feed(self, repos, search_term, public=True):
171 def _rss_feed(self, repos, search_term, public=True):
167 _ = self.request.translate
172 _ = self.request.translate
168 journal = self._get_journal_data(repos, search_term)
173 journal = self._get_journal_data(repos, search_term)
169 if public:
174 if public:
170 _link = h.route_url('journal_public_atom')
175 _link = h.route_url('journal_public_atom')
171 _desc = '%s %s %s' % (
176 _desc = '%s %s %s' % (
172 self.rhodecode_name, _('public journal'), 'rss feed')
177 self.rhodecode_name, _('public journal'), 'rss feed')
173 else:
178 else:
174 _link = h.route_url('journal_atom')
179 _link = h.route_url('journal_atom')
175 _desc = '%s %s %s' % (
180 _desc = '%s %s %s' % (
176 self.rhodecode_name, _('journal'), 'rss feed')
181 self.rhodecode_name, _('journal'), 'rss feed')
177
182
178 feed = Rss201rev2Feed(
183 feed = Rss201rev2Feed(
179 title=_desc, link=_link, description=_desc,
184 title=_desc, link=_link, description=_desc,
180 language=self.language, ttl=self.ttl)
185 language=self.language, ttl=self.ttl)
181
186
182 for entry in journal[:self.feed_items_per_page]:
187 for entry in journal[:self.feed_items_per_page]:
183 user = entry.user
188 user = entry.user
184 if user is None:
189 if user is None:
185 # fix deleted users
190 # fix deleted users
186 user = AttributeDict({'short_contact': entry.username,
191 user = AttributeDict({'short_contact': entry.username,
187 'email': '',
192 'email': '',
188 'full_contact': ''})
193 'full_contact': ''})
189 action, action_extra, ico = h.action_parser(entry, feed=True)
194 action, action_extra, ico = h.action_parser(entry, feed=True)
190 title = "%s - %s %s" % (user.short_contact, action(),
195 title = "%s - %s %s" % (user.short_contact, action(),
191 entry.repository.repo_name)
196 entry.repository.repo_name)
192 desc = action_extra()
197 desc = action_extra()
193 _url = h.route_url('home')
198 _url = h.route_url('home')
194 if entry.repository is not None:
199 if entry.repository is not None:
195 _url = h.route_url('repo_changelog',
200 _url = h.route_url('repo_changelog',
196 repo_name=entry.repository.repo_name)
201 repo_name=entry.repository.repo_name)
197
202
198 feed.add_item(title=title,
203 feed.add_item(
199 pubdate=entry.action_date,
204 unique_id=self.feed_uid(entry.user_log_id),
200 link=_url,
205 title=title,
201 author_email=user.email,
206 pubdate=entry.action_date,
202 author_name=user.full_contact,
207 link=_url,
203 description=desc)
208 author_email=user.email,
209 author_name=user.full_contact,
210 description=desc)
204
211
205 response = Response(feed.writeString('utf-8'))
212 response = Response(feed.writeString('utf-8'))
206 response.content_type = feed.mime_type
213 response.content_type = feed.mime_type
207 return response
214 return response
208
215
209 @LoginRequired()
216 @LoginRequired()
210 @NotAnonymous()
217 @NotAnonymous()
211 @view_config(
218 @view_config(
212 route_name='journal', request_method='GET',
219 route_name='journal', request_method='GET',
213 renderer=None)
220 renderer=None)
214 def journal(self):
221 def journal(self):
215 c = self.load_default_context()
222 c = self.load_default_context()
216
223
217 p = safe_int(self.request.GET.get('page', 1), 1)
224 p = safe_int(self.request.GET.get('page', 1), 1)
218 c.user = User.get(self._rhodecode_user.user_id)
225 c.user = User.get(self._rhodecode_user.user_id)
219 following = Session().query(UserFollowing)\
226 following = Session().query(UserFollowing)\
220 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
227 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
221 .options(joinedload(UserFollowing.follows_repository))\
228 .options(joinedload(UserFollowing.follows_repository))\
222 .all()
229 .all()
223
230
224 journal = self._get_journal_data(following, c.search_term)
231 journal = self._get_journal_data(following, c.search_term)
225
232
226 def url_generator(**kw):
233 def url_generator(**kw):
227 query_params = {
234 query_params = {
228 'filter': c.search_term
235 'filter': c.search_term
229 }
236 }
230 query_params.update(kw)
237 query_params.update(kw)
231 return self.request.current_route_path(_query=query_params)
238 return self.request.current_route_path(_query=query_params)
232
239
233 c.journal_pager = Page(
240 c.journal_pager = Page(
234 journal, page=p, items_per_page=20, url=url_generator)
241 journal, page=p, items_per_page=20, url=url_generator)
235 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
242 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
236
243
237 c.journal_data = render(
244 c.journal_data = render(
238 'rhodecode:templates/journal/journal_data.mako',
245 'rhodecode:templates/journal/journal_data.mako',
239 self._get_template_context(c), self.request)
246 self._get_template_context(c), self.request)
240
247
241 if self.request.is_xhr:
248 if self.request.is_xhr:
242 return Response(c.journal_data)
249 return Response(c.journal_data)
243
250
244 html = render(
251 html = render(
245 'rhodecode:templates/journal/journal.mako',
252 'rhodecode:templates/journal/journal.mako',
246 self._get_template_context(c), self.request)
253 self._get_template_context(c), self.request)
247 return Response(html)
254 return Response(html)
248
255
249 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
256 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
250 @NotAnonymous()
257 @NotAnonymous()
251 @view_config(
258 @view_config(
252 route_name='journal_atom', request_method='GET',
259 route_name='journal_atom', request_method='GET',
253 renderer=None)
260 renderer=None)
254 def journal_atom(self):
261 def journal_atom(self):
255 """
262 """
256 Produce an atom-1.0 feed via feedgenerator module
263 Produce an atom-1.0 feed via feedgenerator module
257 """
264 """
258 c = self.load_default_context()
265 c = self.load_default_context()
259 following_repos = Session().query(UserFollowing)\
266 following_repos = Session().query(UserFollowing)\
260 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
267 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
261 .options(joinedload(UserFollowing.follows_repository))\
268 .options(joinedload(UserFollowing.follows_repository))\
262 .all()
269 .all()
263 return self._atom_feed(following_repos, c.search_term, public=False)
270 return self._atom_feed(following_repos, c.search_term, public=False)
264
271
265 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
272 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
266 @NotAnonymous()
273 @NotAnonymous()
267 @view_config(
274 @view_config(
268 route_name='journal_rss', request_method='GET',
275 route_name='journal_rss', request_method='GET',
269 renderer=None)
276 renderer=None)
270 def journal_rss(self):
277 def journal_rss(self):
271 """
278 """
272 Produce an rss feed via feedgenerator module
279 Produce an rss feed via feedgenerator module
273 """
280 """
274 c = self.load_default_context()
281 c = self.load_default_context()
275 following_repos = Session().query(UserFollowing)\
282 following_repos = Session().query(UserFollowing)\
276 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
283 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
277 .options(joinedload(UserFollowing.follows_repository))\
284 .options(joinedload(UserFollowing.follows_repository))\
278 .all()
285 .all()
279 return self._rss_feed(following_repos, c.search_term, public=False)
286 return self._rss_feed(following_repos, c.search_term, public=False)
280
287
281 @LoginRequired()
288 @LoginRequired()
282 @NotAnonymous()
289 @NotAnonymous()
283 @CSRFRequired()
290 @CSRFRequired()
284 @view_config(
291 @view_config(
285 route_name='toggle_following', request_method='POST',
292 route_name='toggle_following', request_method='POST',
286 renderer='json_ext')
293 renderer='json_ext')
287 def toggle_following(self):
294 def toggle_following(self):
288 user_id = self.request.POST.get('follows_user_id')
295 user_id = self.request.POST.get('follows_user_id')
289 if user_id:
296 if user_id:
290 try:
297 try:
291 ScmModel().toggle_following_user(
298 ScmModel().toggle_following_user(
292 user_id, self._rhodecode_user.user_id)
299 user_id, self._rhodecode_user.user_id)
293 Session().commit()
300 Session().commit()
294 return 'ok'
301 return 'ok'
295 except Exception:
302 except Exception:
296 raise HTTPBadRequest()
303 raise HTTPBadRequest()
297
304
298 repo_id = self.request.POST.get('follows_repo_id')
305 repo_id = self.request.POST.get('follows_repo_id')
299 if repo_id:
306 if repo_id:
300 try:
307 try:
301 ScmModel().toggle_following_repo(
308 ScmModel().toggle_following_repo(
302 repo_id, self._rhodecode_user.user_id)
309 repo_id, self._rhodecode_user.user_id)
303 Session().commit()
310 Session().commit()
304 return 'ok'
311 return 'ok'
305 except Exception:
312 except Exception:
306 raise HTTPBadRequest()
313 raise HTTPBadRequest()
307
314
308 raise HTTPBadRequest()
315 raise HTTPBadRequest()
309
316
310 @LoginRequired()
317 @LoginRequired()
311 @view_config(
318 @view_config(
312 route_name='journal_public', request_method='GET',
319 route_name='journal_public', request_method='GET',
313 renderer=None)
320 renderer=None)
314 def journal_public(self):
321 def journal_public(self):
315 c = self.load_default_context()
322 c = self.load_default_context()
316 # Return a rendered template
323 # Return a rendered template
317 p = safe_int(self.request.GET.get('page', 1), 1)
324 p = safe_int(self.request.GET.get('page', 1), 1)
318
325
319 c.following = Session().query(UserFollowing)\
326 c.following = Session().query(UserFollowing)\
320 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
327 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
321 .options(joinedload(UserFollowing.follows_repository))\
328 .options(joinedload(UserFollowing.follows_repository))\
322 .all()
329 .all()
323
330
324 journal = self._get_journal_data(c.following, c.search_term)
331 journal = self._get_journal_data(c.following, c.search_term)
325
332
326 def url_generator(**kw):
333 def url_generator(**kw):
327 query_params = {}
334 query_params = {}
328 query_params.update(kw)
335 query_params.update(kw)
329 return self.request.current_route_path(_query=query_params)
336 return self.request.current_route_path(_query=query_params)
330
337
331 c.journal_pager = Page(
338 c.journal_pager = Page(
332 journal, page=p, items_per_page=20, url=url_generator)
339 journal, page=p, items_per_page=20, url=url_generator)
333 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
340 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
334
341
335 c.journal_data = render(
342 c.journal_data = render(
336 'rhodecode:templates/journal/journal_data.mako',
343 'rhodecode:templates/journal/journal_data.mako',
337 self._get_template_context(c), self.request)
344 self._get_template_context(c), self.request)
338
345
339 if self.request.is_xhr:
346 if self.request.is_xhr:
340 return Response(c.journal_data)
347 return Response(c.journal_data)
341
348
342 html = render(
349 html = render(
343 'rhodecode:templates/journal/public_journal.mako',
350 'rhodecode:templates/journal/public_journal.mako',
344 self._get_template_context(c), self.request)
351 self._get_template_context(c), self.request)
345 return Response(html)
352 return Response(html)
346
353
347 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
354 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
348 @view_config(
355 @view_config(
349 route_name='journal_public_atom', request_method='GET',
356 route_name='journal_public_atom', request_method='GET',
350 renderer=None)
357 renderer=None)
351 def journal_public_atom(self):
358 def journal_public_atom(self):
352 """
359 """
353 Produce an atom-1.0 feed via feedgenerator module
360 Produce an atom-1.0 feed via feedgenerator module
354 """
361 """
355 c = self.load_default_context()
362 c = self.load_default_context()
356 following_repos = Session().query(UserFollowing)\
363 following_repos = Session().query(UserFollowing)\
357 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
364 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
358 .options(joinedload(UserFollowing.follows_repository))\
365 .options(joinedload(UserFollowing.follows_repository))\
359 .all()
366 .all()
360
367
361 return self._atom_feed(following_repos, c.search_term)
368 return self._atom_feed(following_repos, c.search_term)
362
369
363 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
370 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
364 @view_config(
371 @view_config(
365 route_name='journal_public_rss', request_method='GET',
372 route_name='journal_public_rss', request_method='GET',
366 renderer=None)
373 renderer=None)
367 def journal_public_rss(self):
374 def journal_public_rss(self):
368 """
375 """
369 Produce an rss2 feed via feedgenerator module
376 Produce an rss2 feed via feedgenerator module
370 """
377 """
371 c = self.load_default_context()
378 c = self.load_default_context()
372 following_repos = Session().query(UserFollowing)\
379 following_repos = Session().query(UserFollowing)\
373 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
380 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
374 .options(joinedload(UserFollowing.follows_repository))\
381 .options(joinedload(UserFollowing.follows_repository))\
375 .all()
382 .all()
376
383
377 return self._rss_feed(following_repos, c.search_term)
384 return self._rss_feed(following_repos, c.search_term)
@@ -1,202 +1,207 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2017-2017 RhodeCode GmbH
3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytz
21 import pytz
22 import logging
22 import logging
23
23
24 from beaker.cache import cache_region
24 from beaker.cache import cache_region
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.response import Response
26 from pyramid.response import Response
27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.lib import audit_logger
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
33 LoginRequired, HasRepoPermissionAnyDecorator)
33 LoginRequired, HasRepoPermissionAnyDecorator)
34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
35 from rhodecode.lib.utils2 import str2bool, safe_int
35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
36 from rhodecode.model.db import UserApiKeys, CacheKey
36 from rhodecode.model.db import UserApiKeys, CacheKey
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class RepoFeedView(RepoAppView):
41 class RepoFeedView(RepoAppView):
42 def load_default_context(self):
42 def load_default_context(self):
43 c = self._get_local_tmpl_context()
43 c = self._get_local_tmpl_context()
44
44
45 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
45 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
46 c.repo_info = self.db_repo
46 c.repo_info = self.db_repo
47
47
48 self._register_global_c(c)
48 self._register_global_c(c)
49 self._load_defaults()
49 self._load_defaults()
50 return c
50 return c
51
51
52 def _get_config(self):
52 def _get_config(self):
53 import rhodecode
53 import rhodecode
54 config = rhodecode.CONFIG
54 config = rhodecode.CONFIG
55
55
56 return {
56 return {
57 'language': 'en-us',
57 'language': 'en-us',
58 'feed_ttl': '5', # TTL of feed,
58 'feed_ttl': '5', # TTL of feed,
59 'feed_include_diff':
59 'feed_include_diff':
60 str2bool(config.get('rss_include_diff', False)),
60 str2bool(config.get('rss_include_diff', False)),
61 'feed_items_per_page':
61 'feed_items_per_page':
62 safe_int(config.get('rss_items_per_page', 20)),
62 safe_int(config.get('rss_items_per_page', 20)),
63 'feed_diff_limit':
63 'feed_diff_limit':
64 # we need to protect from parsing huge diffs here other way
64 # we need to protect from parsing huge diffs here other way
65 # we can kill the server
65 # we can kill the server
66 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
66 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
67 }
67 }
68
68
69 def _load_defaults(self):
69 def _load_defaults(self):
70 _ = self.request.translate
70 _ = self.request.translate
71 config = self._get_config()
71 config = self._get_config()
72 # common values for feeds
72 # common values for feeds
73 self.description = _('Changes on %s repository')
73 self.description = _('Changes on %s repository')
74 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
74 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
75 self.language = config["language"]
75 self.language = config["language"]
76 self.ttl = config["feed_ttl"]
76 self.ttl = config["feed_ttl"]
77 self.feed_include_diff = config['feed_include_diff']
77 self.feed_include_diff = config['feed_include_diff']
78 self.feed_diff_limit = config['feed_diff_limit']
78 self.feed_diff_limit = config['feed_diff_limit']
79 self.feed_items_per_page = config['feed_items_per_page']
79 self.feed_items_per_page = config['feed_items_per_page']
80
80
81 def _changes(self, commit):
81 def _changes(self, commit):
82 diff_processor = DiffProcessor(
82 diff_processor = DiffProcessor(
83 commit.diff(), diff_limit=self.feed_diff_limit)
83 commit.diff(), diff_limit=self.feed_diff_limit)
84 _parsed = diff_processor.prepare(inline_diff=False)
84 _parsed = diff_processor.prepare(inline_diff=False)
85 limited_diff = isinstance(_parsed, LimitedDiffContainer)
85 limited_diff = isinstance(_parsed, LimitedDiffContainer)
86
86
87 return _parsed, limited_diff
87 return _parsed, limited_diff
88
88
89 def _get_title(self, commit):
89 def _get_title(self, commit):
90 return h.shorter(commit.message, 160)
90 return h.shorter(commit.message, 160)
91
91
92 def _get_description(self, commit):
92 def _get_description(self, commit):
93 _renderer = self.request.get_partial_renderer(
93 _renderer = self.request.get_partial_renderer(
94 'feed/atom_feed_entry.mako')
94 'feed/atom_feed_entry.mako')
95 parsed_diff, limited_diff = self._changes(commit)
95 parsed_diff, limited_diff = self._changes(commit)
96 return _renderer(
96 return _renderer(
97 'body',
97 'body',
98 commit=commit,
98 commit=commit,
99 parsed_diff=parsed_diff,
99 parsed_diff=parsed_diff,
100 limited_diff=limited_diff,
100 limited_diff=limited_diff,
101 feed_include_diff=self.feed_include_diff,
101 feed_include_diff=self.feed_include_diff,
102 )
102 )
103
103
104 def _set_timezone(self, date, tzinfo=pytz.utc):
104 def _set_timezone(self, date, tzinfo=pytz.utc):
105 if not getattr(date, "tzinfo", None):
105 if not getattr(date, "tzinfo", None):
106 date.replace(tzinfo=tzinfo)
106 date.replace(tzinfo=tzinfo)
107 return date
107 return date
108
108
109 def _get_commits(self):
109 def _get_commits(self):
110 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
110 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
111
111
112 def uid(self, repo_id, commit_id):
113 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
114
112 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
115 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
113 @HasRepoPermissionAnyDecorator(
116 @HasRepoPermissionAnyDecorator(
114 'repository.read', 'repository.write', 'repository.admin')
117 'repository.read', 'repository.write', 'repository.admin')
115 @view_config(
118 @view_config(
116 route_name='atom_feed_home', request_method='GET',
119 route_name='atom_feed_home', request_method='GET',
117 renderer=None)
120 renderer=None)
118 def atom(self):
121 def atom(self):
119 """
122 """
120 Produce an atom-1.0 feed via feedgenerator module
123 Produce an atom-1.0 feed via feedgenerator module
121 """
124 """
122 self.load_default_context()
125 self.load_default_context()
123
126
124 @cache_region('long_term')
127 @cache_region('long_term')
125 def _generate_feed(cache_key):
128 def _generate_feed(cache_key):
126 feed = Atom1Feed(
129 feed = Atom1Feed(
127 title=self.title % self.db_repo_name,
130 title=self.title % self.db_repo_name,
128 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
131 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
129 description=self.description % self.db_repo_name,
132 description=self.description % self.db_repo_name,
130 language=self.language,
133 language=self.language,
131 ttl=self.ttl
134 ttl=self.ttl
132 )
135 )
133
136
134 for commit in reversed(self._get_commits()):
137 for commit in reversed(self._get_commits()):
135 date = self._set_timezone(commit.date)
138 date = self._set_timezone(commit.date)
136 feed.add_item(
139 feed.add_item(
140 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
137 title=self._get_title(commit),
141 title=self._get_title(commit),
138 author_name=commit.author,
142 author_name=commit.author,
139 description=self._get_description(commit),
143 description=self._get_description(commit),
140 link=h.route_url(
144 link=h.route_url(
141 'repo_commit', repo_name=self.db_repo_name,
145 'repo_commit', repo_name=self.db_repo_name,
142 commit_id=commit.raw_id),
146 commit_id=commit.raw_id),
143 pubdate=date,)
147 pubdate=date,)
144
148
145 return feed.mime_type, feed.writeString('utf-8')
149 return feed.mime_type, feed.writeString('utf-8')
146
150
147 invalidator_context = CacheKey.repo_context_cache(
151 invalidator_context = CacheKey.repo_context_cache(
148 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
152 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
149
153
150 with invalidator_context as context:
154 with invalidator_context as context:
151 context.invalidate()
155 context.invalidate()
152 mime_type, feed = context.compute()
156 mime_type, feed = context.compute()
153
157
154 response = Response(feed)
158 response = Response(feed)
155 response.content_type = mime_type
159 response.content_type = mime_type
156 return response
160 return response
157
161
158 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
162 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
159 @HasRepoPermissionAnyDecorator(
163 @HasRepoPermissionAnyDecorator(
160 'repository.read', 'repository.write', 'repository.admin')
164 'repository.read', 'repository.write', 'repository.admin')
161 @view_config(
165 @view_config(
162 route_name='rss_feed_home', request_method='GET',
166 route_name='rss_feed_home', request_method='GET',
163 renderer=None)
167 renderer=None)
164 def rss(self):
168 def rss(self):
165 """
169 """
166 Produce an rss2 feed via feedgenerator module
170 Produce an rss2 feed via feedgenerator module
167 """
171 """
168 self.load_default_context()
172 self.load_default_context()
169
173
170 @cache_region('long_term')
174 @cache_region('long_term')
171 def _generate_feed(cache_key):
175 def _generate_feed(cache_key):
172 feed = Rss201rev2Feed(
176 feed = Rss201rev2Feed(
173 title=self.title % self.db_repo_name,
177 title=self.title % self.db_repo_name,
174 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
178 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
175 description=self.description % self.db_repo_name,
179 description=self.description % self.db_repo_name,
176 language=self.language,
180 language=self.language,
177 ttl=self.ttl
181 ttl=self.ttl
178 )
182 )
179
183
180 for commit in reversed(self._get_commits()):
184 for commit in reversed(self._get_commits()):
181 date = self._set_timezone(commit.date)
185 date = self._set_timezone(commit.date)
182 feed.add_item(
186 feed.add_item(
187 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
183 title=self._get_title(commit),
188 title=self._get_title(commit),
184 author_name=commit.author,
189 author_name=commit.author,
185 description=self._get_description(commit),
190 description=self._get_description(commit),
186 link=h.route_url(
191 link=h.route_url(
187 'repo_commit', repo_name=self.db_repo_name,
192 'repo_commit', repo_name=self.db_repo_name,
188 commit_id=commit.raw_id),
193 commit_id=commit.raw_id),
189 pubdate=date,)
194 pubdate=date,)
190
195
191 return feed.mime_type, feed.writeString('utf-8')
196 return feed.mime_type, feed.writeString('utf-8')
192
197
193 invalidator_context = CacheKey.repo_context_cache(
198 invalidator_context = CacheKey.repo_context_cache(
194 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
199 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
195
200
196 with invalidator_context as context:
201 with invalidator_context as context:
197 context.invalidate()
202 context.invalidate()
198 mime_type, feed = context.compute()
203 mime_type, feed = context.compute()
199
204
200 response = Response(feed)
205 response = Response(feed)
201 response.content_type = mime_type
206 response.content_type = mime_type
202 return response
207 return response
General Comments 0
You need to be logged in to leave comments. Login now