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