##// END OF EJS Templates
webhelpers: extracted django feedgenerator as standalone package to replace webhelpers feedgenerator
dan -
r4092:7d375010 default
parent child Browse files
Show More
@@ -0,0 +1,21 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2019 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 from feedgenerator import Rss201rev2Feed, Atom1Feed No newline at end of file
@@ -0,0 +1,117 b''
1 # Copyright (c) Django Software Foundation and individual contributors.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without modification,
5 # are permitted provided that the following conditions are met:
6 #
7 # 1. Redistributions of source code must retain the above copyright notice,
8 # this list of conditions and the following disclaimer.
9 #
10 # 2. Redistributions in binary form must reproduce the above copyright
11 # notice, this list of conditions and the following disclaimer in the
12 # documentation and/or other materials provided with the distribution.
13 #
14 # 3. Neither the name of Django nor the names of its contributors may be used
15 # to endorse or promote products derived from this software without
16 # specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 # Python's datetime strftime doesn't handle dates before 1900.
30 # These classes override date and datetime to support the formatting of a date
31 # through its full "proleptic Gregorian" date range.
32 #
33 # Based on code submitted to comp.lang.python by Andrew Dalke
34 #
35 # >>> datetime_safe.date(1850, 8, 2).strftime("%Y/%m/%d was a %A")
36 # '1850/08/02 was a Friday'
37
38 from datetime import date as real_date, datetime as real_datetime
39 import re
40 import time
41
42 class date(real_date):
43 def strftime(self, fmt):
44 return strftime(self, fmt)
45
46 class datetime(real_datetime):
47 def strftime(self, fmt):
48 return strftime(self, fmt)
49
50 def combine(self, date, time):
51 return datetime(date.year, date.month, date.day, time.hour, time.minute, time.microsecond, time.tzinfo)
52
53 def date(self):
54 return date(self.year, self.month, self.day)
55
56 def new_date(d):
57 "Generate a safe date from a datetime.date object."
58 return date(d.year, d.month, d.day)
59
60 def new_datetime(d):
61 """
62 Generate a safe datetime from a datetime.date or datetime.datetime object.
63 """
64 kw = [d.year, d.month, d.day]
65 if isinstance(d, real_datetime):
66 kw.extend([d.hour, d.minute, d.second, d.microsecond, d.tzinfo])
67 return datetime(*kw)
68
69 # This library does not support strftime's "%s" or "%y" format strings.
70 # Allowed if there's an even number of "%"s because they are escaped.
71 _illegal_formatting = re.compile(r"((^|[^%])(%%)*%[sy])")
72
73 def _findall(text, substr):
74 # Also finds overlaps
75 sites = []
76 i = 0
77 while 1:
78 j = text.find(substr, i)
79 if j == -1:
80 break
81 sites.append(j)
82 i=j+1
83 return sites
84
85 def strftime(dt, fmt):
86 if dt.year >= 1900:
87 return super(type(dt), dt).strftime(fmt)
88 illegal_formatting = _illegal_formatting.search(fmt)
89 if illegal_formatting:
90 raise TypeError("strftime of dates before 1900 does not handle" + illegal_formatting.group(0))
91
92 year = dt.year
93 # For every non-leap year century, advance by
94 # 6 years to get into the 28-year repeat cycle
95 delta = 2000 - year
96 off = 6 * (delta // 100 + delta // 400)
97 year = year + off
98
99 # Move to around the year 2000
100 year = year + ((2000 - year) // 28) * 28
101 timetuple = dt.timetuple()
102 s1 = time.strftime(fmt, (year,) + timetuple[1:])
103 sites1 = _findall(s1, str(year))
104
105 s2 = time.strftime(fmt, (year+28,) + timetuple[1:])
106 sites2 = _findall(s2, str(year+28))
107
108 sites = []
109 for site in sites1:
110 if site in sites2:
111 sites.append(site)
112
113 s = s1
114 syear = "%04d" % (dt.year,)
115 for site in sites:
116 s = s[:site] + syear + s[site+4:]
117 return s
@@ -0,0 +1,444 b''
1 # Copyright (c) Django Software Foundation and individual contributors.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without modification,
5 # are permitted provided that the following conditions are met:
6 #
7 # 1. Redistributions of source code must retain the above copyright notice,
8 # this list of conditions and the following disclaimer.
9 #
10 # 2. Redistributions in binary form must reproduce the above copyright
11 # notice, this list of conditions and the following disclaimer in the
12 # documentation and/or other materials provided with the distribution.
13 #
14 # 3. Neither the name of Django nor the names of its contributors may be used
15 # to endorse or promote products derived from this software without
16 # specific prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 """
30 For definitions of the different versions of RSS, see:
31 http://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004/02/04/incompatible-rss
32 """
33 from __future__ import unicode_literals
34
35 import datetime
36 from StringIO import StringIO
37 from six.moves.urllib import parse as urlparse
38
39 from rhodecode.lib.feedgenerator import datetime_safe
40 from rhodecode.lib.feedgenerator.utils import SimplerXMLGenerator, iri_to_uri, force_text
41
42
43 #### The following code comes from ``django.utils.feedgenerator`` ####
44
45
46 def rfc2822_date(date):
47 # We can't use strftime() because it produces locale-dependent results, so
48 # we have to map english month and day names manually
49 months = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',)
50 days = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
51 # Support datetime objects older than 1900
52 date = datetime_safe.new_datetime(date)
53 # We do this ourselves to be timezone aware, email.Utils is not tz aware.
54 dow = days[date.weekday()]
55 month = months[date.month - 1]
56 time_str = date.strftime('%s, %%d %s %%Y %%H:%%M:%%S ' % (dow, month))
57
58 time_str = time_str.decode('utf-8')
59 offset = date.utcoffset()
60 # Historically, this function assumes that naive datetimes are in UTC.
61 if offset is None:
62 return time_str + '-0000'
63 else:
64 timezone = (offset.days * 24 * 60) + (offset.seconds // 60)
65 hour, minute = divmod(timezone, 60)
66 return time_str + '%+03d%02d' % (hour, minute)
67
68
69 def rfc3339_date(date):
70 # Support datetime objects older than 1900
71 date = datetime_safe.new_datetime(date)
72 time_str = date.strftime('%Y-%m-%dT%H:%M:%S')
73
74 time_str = time_str.decode('utf-8')
75 offset = date.utcoffset()
76 # Historically, this function assumes that naive datetimes are in UTC.
77 if offset is None:
78 return time_str + 'Z'
79 else:
80 timezone = (offset.days * 24 * 60) + (offset.seconds // 60)
81 hour, minute = divmod(timezone, 60)
82 return time_str + '%+03d:%02d' % (hour, minute)
83
84
85 def get_tag_uri(url, date):
86 """
87 Creates a TagURI.
88
89 See http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id
90 """
91 bits = urlparse(url)
92 d = ''
93 if date is not None:
94 d = ',%s' % datetime_safe.new_datetime(date).strftime('%Y-%m-%d')
95 return 'tag:%s%s:%s/%s' % (bits.hostname, d, bits.path, bits.fragment)
96
97
98 class SyndicationFeed(object):
99 """Base class for all syndication feeds. Subclasses should provide write()"""
100
101 def __init__(self, title, link, description, language=None, author_email=None,
102 author_name=None, author_link=None, subtitle=None, categories=None,
103 feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs):
104 def to_unicode(s):
105 return force_text(s, strings_only=True)
106 if categories:
107 categories = [force_text(c) for c in categories]
108 if ttl is not None:
109 # Force ints to unicode
110 ttl = force_text(ttl)
111 self.feed = {
112 'title': to_unicode(title),
113 'link': iri_to_uri(link),
114 'description': to_unicode(description),
115 'language': to_unicode(language),
116 'author_email': to_unicode(author_email),
117 'author_name': to_unicode(author_name),
118 'author_link': iri_to_uri(author_link),
119 'subtitle': to_unicode(subtitle),
120 'categories': categories or (),
121 'feed_url': iri_to_uri(feed_url),
122 'feed_copyright': to_unicode(feed_copyright),
123 'id': feed_guid or link,
124 'ttl': ttl,
125 }
126 self.feed.update(kwargs)
127 self.items = []
128
129 def add_item(self, title, link, description, author_email=None,
130 author_name=None, author_link=None, pubdate=None, comments=None,
131 unique_id=None, unique_id_is_permalink=None, enclosure=None,
132 categories=(), item_copyright=None, ttl=None, updateddate=None,
133 enclosures=None, **kwargs):
134 """
135 Adds an item to the feed. All args are expected to be Python Unicode
136 objects except pubdate and updateddate, which are datetime.datetime
137 objects, and enclosures, which is an iterable of instances of the
138 Enclosure class.
139 """
140 def to_unicode(s):
141 return force_text(s, strings_only=True)
142 if categories:
143 categories = [to_unicode(c) for c in categories]
144 if ttl is not None:
145 # Force ints to unicode
146 ttl = force_text(ttl)
147 if enclosure is None:
148 enclosures = [] if enclosures is None else enclosures
149
150 item = {
151 'title': to_unicode(title),
152 'link': iri_to_uri(link),
153 'description': to_unicode(description),
154 'author_email': to_unicode(author_email),
155 'author_name': to_unicode(author_name),
156 'author_link': iri_to_uri(author_link),
157 'pubdate': pubdate,
158 'updateddate': updateddate,
159 'comments': to_unicode(comments),
160 'unique_id': to_unicode(unique_id),
161 'unique_id_is_permalink': unique_id_is_permalink,
162 'enclosures': enclosures,
163 'categories': categories or (),
164 'item_copyright': to_unicode(item_copyright),
165 'ttl': ttl,
166 }
167 item.update(kwargs)
168 self.items.append(item)
169
170 def num_items(self):
171 return len(self.items)
172
173 def root_attributes(self):
174 """
175 Return extra attributes to place on the root (i.e. feed/channel) element.
176 Called from write().
177 """
178 return {}
179
180 def add_root_elements(self, handler):
181 """
182 Add elements in the root (i.e. feed/channel) element. Called
183 from write().
184 """
185 pass
186
187 def item_attributes(self, item):
188 """
189 Return extra attributes to place on each item (i.e. item/entry) element.
190 """
191 return {}
192
193 def add_item_elements(self, handler, item):
194 """
195 Add elements on each item (i.e. item/entry) element.
196 """
197 pass
198
199 def write(self, outfile, encoding):
200 """
201 Outputs the feed in the given encoding to outfile, which is a file-like
202 object. Subclasses should override this.
203 """
204 raise NotImplementedError('subclasses of SyndicationFeed must provide a write() method')
205
206 def writeString(self, encoding):
207 """
208 Returns the feed in the given encoding as a string.
209 """
210 s = StringIO()
211 self.write(s, encoding)
212 return s.getvalue()
213
214 def latest_post_date(self):
215 """
216 Returns the latest item's pubdate or updateddate. If no items
217 have either of these attributes this returns the current UTC date/time.
218 """
219 latest_date = None
220 date_keys = ('updateddate', 'pubdate')
221
222 for item in self.items:
223 for date_key in date_keys:
224 item_date = item.get(date_key)
225 if item_date:
226 if latest_date is None or item_date > latest_date:
227 latest_date = item_date
228
229 # datetime.now(tz=utc) is slower, as documented in django.utils.timezone.now
230 return latest_date or datetime.datetime.utcnow().replace(tzinfo=utc)
231
232
233 class Enclosure(object):
234 "Represents an RSS enclosure"
235 def __init__(self, url, length, mime_type):
236 "All args are expected to be Python Unicode objects"
237 self.length, self.mime_type = length, mime_type
238 self.url = iri_to_uri(url)
239
240
241 class RssFeed(SyndicationFeed):
242 content_type = 'application/rss+xml; charset=utf-8'
243
244 def write(self, outfile, encoding):
245 handler = SimplerXMLGenerator(outfile, encoding)
246 handler.startDocument()
247 handler.startElement("rss", self.rss_attributes())
248 handler.startElement("channel", self.root_attributes())
249 self.add_root_elements(handler)
250 self.write_items(handler)
251 self.endChannelElement(handler)
252 handler.endElement("rss")
253
254 def rss_attributes(self):
255 return {"version": self._version,
256 "xmlns:atom": "http://www.w3.org/2005/Atom"}
257
258 def write_items(self, handler):
259 for item in self.items:
260 handler.startElement('item', self.item_attributes(item))
261 self.add_item_elements(handler, item)
262 handler.endElement("item")
263
264 def add_root_elements(self, handler):
265 handler.addQuickElement("title", self.feed['title'])
266 handler.addQuickElement("link", self.feed['link'])
267 handler.addQuickElement("description", self.feed['description'])
268 if self.feed['feed_url'] is not None:
269 handler.addQuickElement("atom:link", None, {"rel": "self", "href": self.feed['feed_url']})
270 if self.feed['language'] is not None:
271 handler.addQuickElement("language", self.feed['language'])
272 for cat in self.feed['categories']:
273 handler.addQuickElement("category", cat)
274 if self.feed['feed_copyright'] is not None:
275 handler.addQuickElement("copyright", self.feed['feed_copyright'])
276 handler.addQuickElement("lastBuildDate", rfc2822_date(self.latest_post_date()))
277 if self.feed['ttl'] is not None:
278 handler.addQuickElement("ttl", self.feed['ttl'])
279
280 def endChannelElement(self, handler):
281 handler.endElement("channel")
282
283
284 class RssUserland091Feed(RssFeed):
285 _version = "0.91"
286
287 def add_item_elements(self, handler, item):
288 handler.addQuickElement("title", item['title'])
289 handler.addQuickElement("link", item['link'])
290 if item['description'] is not None:
291 handler.addQuickElement("description", item['description'])
292
293
294 class Rss201rev2Feed(RssFeed):
295 # Spec: http://blogs.law.harvard.edu/tech/rss
296 _version = "2.0"
297
298 def add_item_elements(self, handler, item):
299 handler.addQuickElement("title", item['title'])
300 handler.addQuickElement("link", item['link'])
301 if item['description'] is not None:
302 handler.addQuickElement("description", item['description'])
303
304 # Author information.
305 if item["author_name"] and item["author_email"]:
306 handler.addQuickElement("author", "%s (%s)" % (item['author_email'], item['author_name']))
307 elif item["author_email"]:
308 handler.addQuickElement("author", item["author_email"])
309 elif item["author_name"]:
310 handler.addQuickElement(
311 "dc:creator", item["author_name"], {"xmlns:dc": "http://purl.org/dc/elements/1.1/"}
312 )
313
314 if item['pubdate'] is not None:
315 handler.addQuickElement("pubDate", rfc2822_date(item['pubdate']))
316 if item['comments'] is not None:
317 handler.addQuickElement("comments", item['comments'])
318 if item['unique_id'] is not None:
319 guid_attrs = {}
320 if isinstance(item.get('unique_id_is_permalink'), bool):
321 guid_attrs['isPermaLink'] = str(item['unique_id_is_permalink']).lower()
322 handler.addQuickElement("guid", item['unique_id'], guid_attrs)
323 if item['ttl'] is not None:
324 handler.addQuickElement("ttl", item['ttl'])
325
326 # Enclosure.
327 if item['enclosures']:
328 enclosures = list(item['enclosures'])
329 if len(enclosures) > 1:
330 raise ValueError(
331 "RSS feed items may only have one enclosure, see "
332 "http://www.rssboard.org/rss-profile#element-channel-item-enclosure"
333 )
334 enclosure = enclosures[0]
335 handler.addQuickElement('enclosure', '', {
336 'url': enclosure.url,
337 'length': enclosure.length,
338 'type': enclosure.mime_type,
339 })
340
341 # Categories.
342 for cat in item['categories']:
343 handler.addQuickElement("category", cat)
344
345
346 class Atom1Feed(SyndicationFeed):
347 # Spec: https://tools.ietf.org/html/rfc4287
348 content_type = 'application/atom+xml; charset=utf-8'
349 ns = "http://www.w3.org/2005/Atom"
350
351 def write(self, outfile, encoding):
352 handler = SimplerXMLGenerator(outfile, encoding)
353 handler.startDocument()
354 handler.startElement('feed', self.root_attributes())
355 self.add_root_elements(handler)
356 self.write_items(handler)
357 handler.endElement("feed")
358
359 def root_attributes(self):
360 if self.feed['language'] is not None:
361 return {"xmlns": self.ns, "xml:lang": self.feed['language']}
362 else:
363 return {"xmlns": self.ns}
364
365 def add_root_elements(self, handler):
366 handler.addQuickElement("title", self.feed['title'])
367 handler.addQuickElement("link", "", {"rel": "alternate", "href": self.feed['link']})
368 if self.feed['feed_url'] is not None:
369 handler.addQuickElement("link", "", {"rel": "self", "href": self.feed['feed_url']})
370 handler.addQuickElement("id", self.feed['id'])
371 handler.addQuickElement("updated", rfc3339_date(self.latest_post_date()))
372 if self.feed['author_name'] is not None:
373 handler.startElement("author", {})
374 handler.addQuickElement("name", self.feed['author_name'])
375 if self.feed['author_email'] is not None:
376 handler.addQuickElement("email", self.feed['author_email'])
377 if self.feed['author_link'] is not None:
378 handler.addQuickElement("uri", self.feed['author_link'])
379 handler.endElement("author")
380 if self.feed['subtitle'] is not None:
381 handler.addQuickElement("subtitle", self.feed['subtitle'])
382 for cat in self.feed['categories']:
383 handler.addQuickElement("category", "", {"term": cat})
384 if self.feed['feed_copyright'] is not None:
385 handler.addQuickElement("rights", self.feed['feed_copyright'])
386
387 def write_items(self, handler):
388 for item in self.items:
389 handler.startElement("entry", self.item_attributes(item))
390 self.add_item_elements(handler, item)
391 handler.endElement("entry")
392
393 def add_item_elements(self, handler, item):
394 handler.addQuickElement("title", item['title'])
395 handler.addQuickElement("link", "", {"href": item['link'], "rel": "alternate"})
396
397 if item['pubdate'] is not None:
398 handler.addQuickElement('published', rfc3339_date(item['pubdate']))
399
400 if item['updateddate'] is not None:
401 handler.addQuickElement('updated', rfc3339_date(item['updateddate']))
402
403 # Author information.
404 if item['author_name'] is not None:
405 handler.startElement("author", {})
406 handler.addQuickElement("name", item['author_name'])
407 if item['author_email'] is not None:
408 handler.addQuickElement("email", item['author_email'])
409 if item['author_link'] is not None:
410 handler.addQuickElement("uri", item['author_link'])
411 handler.endElement("author")
412
413 # Unique ID.
414 if item['unique_id'] is not None:
415 unique_id = item['unique_id']
416 else:
417 unique_id = get_tag_uri(item['link'], item['pubdate'])
418 handler.addQuickElement("id", unique_id)
419
420 # Summary.
421 if item['description'] is not None:
422 handler.addQuickElement("summary", item['description'], {"type": "html"})
423
424 # Enclosures.
425 for enclosure in item['enclosures']:
426 handler.addQuickElement('link', '', {
427 'rel': 'enclosure',
428 'href': enclosure.url,
429 'length': enclosure.length,
430 'type': enclosure.mime_type,
431 })
432
433 # Categories.
434 for cat in item['categories']:
435 handler.addQuickElement("category", "", {"term": cat})
436
437 # Rights.
438 if item['item_copyright'] is not None:
439 handler.addQuickElement("rights", item['item_copyright'])
440
441
442 # This isolates the decision of what the system default is, so calling code can
443 # do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
444 DefaultFeed = Rss201rev2Feed No newline at end of file
@@ -0,0 +1,57 b''
1 """
2 Utilities for XML generation/parsing.
3 """
4
5 import six
6
7 from xml.sax.saxutils import XMLGenerator, quoteattr
8 from urllib import quote
9 from rhodecode.lib.utils import safe_str, safe_unicode
10
11
12 class SimplerXMLGenerator(XMLGenerator):
13 def addQuickElement(self, name, contents=None, attrs=None):
14 "Convenience method for adding an element with no children"
15 if attrs is None:
16 attrs = {}
17 self.startElement(name, attrs)
18 if contents is not None:
19 self.characters(contents)
20 self.endElement(name)
21
22 def startElement(self, name, attrs):
23 self._write('<' + name)
24 # sort attributes for consistent output
25 for (name, value) in sorted(attrs.items()):
26 self._write(' %s=%s' % (name, quoteattr(value)))
27 self._write(six.u('>'))
28
29
30 def iri_to_uri(iri):
31 """
32 Convert an Internationalized Resource Identifier (IRI) portion to a URI
33 portion that is suitable for inclusion in a URL.
34 This is the algorithm from section 3.1 of RFC 3987. However, since we are
35 assuming input is either UTF-8 or unicode already, we can simplify things a
36 little from the full method.
37 Returns an ASCII string containing the encoded result.
38 """
39 # The list of safe characters here is constructed from the "reserved" and
40 # "unreserved" characters specified in sections 2.2 and 2.3 of RFC 3986:
41 # reserved = gen-delims / sub-delims
42 # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
43 # sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
44 # / "*" / "+" / "," / ";" / "="
45 # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
46 # Of the unreserved characters, urllib.quote already considers all but
47 # the ~ safe.
48 # The % character is also added to the list of safe characters here, as the
49 # end of section 3.1 of RFC 3987 specifically mentions that % must not be
50 # converted.
51 if iri is None:
52 return iri
53 return quote(safe_str(iri), safe=b"/#%[]=:;$&()+,!?*@'~")
54
55
56 def force_text(text, strings_only=False):
57 return safe_unicode(text)
@@ -22,7 +22,7 b''
22 import logging
22 import logging
23 import itertools
23 import itertools
24
24
25 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
25
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
@@ -38,6 +38,7 b' from rhodecode.lib.helpers import SqlPag'
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, HasRepoPermissionAny
39 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, HasRepoPermissionAny
40 from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe
40 from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe
41 from rhodecode.lib.feedgenerator.feedgenerator import Atom1Feed, Rss201rev2Feed
41 from rhodecode.model.scm import ScmModel
42 from rhodecode.model.scm import ScmModel
42
43
43 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
@@ -166,7 +167,7 b' class JournalView(BaseAppView):'
166 description=desc)
167 description=desc)
167
168
168 response = Response(feed.writeString('utf-8'))
169 response = Response(feed.writeString('utf-8'))
169 response.content_type = feed.mime_type
170 response.content_type = feed.content_type
170 return response
171 return response
171
172
172 def _rss_feed(self, repos, search_term, public=True):
173 def _rss_feed(self, repos, search_term, public=True):
@@ -212,7 +213,7 b' class JournalView(BaseAppView):'
212 description=desc)
213 description=desc)
213
214
214 response = Response(feed.writeString('utf-8'))
215 response = Response(feed.writeString('utf-8'))
215 response.content_type = feed.mime_type
216 response.content_type = feed.content_type
216 return response
217 return response
217
218
218 @LoginRequired()
219 @LoginRequired()
@@ -41,7 +41,7 b' def route_path(name, params=None, **kwar'
41 class TestFeedView(TestController):
41 class TestFeedView(TestController):
42
42
43 @pytest.mark.parametrize("feed_type,response_types,content_type",[
43 @pytest.mark.parametrize("feed_type,response_types,content_type",[
44 ('rss', ['<rss version="2.0">'],
44 ('rss', ['<rss version="2.0"'],
45 "application/rss+xml"),
45 "application/rss+xml"),
46 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
46 ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
47 "application/atom+xml"),
47 "application/atom+xml"),
@@ -22,9 +22,10 b' import logging'
22
22
23 from pyramid.view import view_config
23 from pyramid.view import view_config
24 from pyramid.response import Response
24 from pyramid.response import Response
25 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
25
26
26
27 from rhodecode.apps._base import RepoAppView
27 from rhodecode.apps._base import RepoAppView
28 from rhodecode.lib.feedgenerator import Rss201rev2Feed, Atom1Feed
28 from rhodecode.lib import audit_logger
29 from rhodecode.lib import audit_logger
29 from rhodecode.lib import rc_cache
30 from rhodecode.lib import rc_cache
30 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
@@ -65,7 +66,7 b' class RepoFeedView(RepoAppView):'
65 config = self._get_config()
66 config = self._get_config()
66 # common values for feeds
67 # common values for feeds
67 self.description = _('Changes on %s repository')
68 self.description = _('Changes on %s repository')
68 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
69 self.title = _('%s %s feed') % (self.db_repo_name, '%s')
69 self.language = config["language"]
70 self.language = config["language"]
70 self.ttl = config["feed_ttl"]
71 self.ttl = config["feed_ttl"]
71 self.feed_include_diff = config['feed_include_diff']
72 self.feed_include_diff = config['feed_include_diff']
@@ -81,7 +82,7 b' class RepoFeedView(RepoAppView):'
81 return diff_processor, _parsed, limited_diff
82 return diff_processor, _parsed, limited_diff
82
83
83 def _get_title(self, commit):
84 def _get_title(self, commit):
84 return h.shorter(commit.message, 160)
85 return h.chop_at_smart(commit.message, '\n', suffix_if_chopped='...')
85
86
86 def _get_description(self, commit):
87 def _get_description(self, commit):
87 _renderer = self.request.get_partial_renderer(
88 _renderer = self.request.get_partial_renderer(
@@ -104,7 +105,12 b' class RepoFeedView(RepoAppView):'
104 return date
105 return date
105
106
106 def _get_commits(self):
107 def _get_commits(self):
107 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
108 pre_load = ['author', 'branch', 'date', 'message', 'parents']
109 collection = self.rhodecode_vcs_repo.get_commits(
110 branch_name=None, show_hidden=False, pre_load=pre_load,
111 translate_tags=False)
112
113 return list(collection[-self.feed_items_per_page:])
108
114
109 def uid(self, repo_id, commit_id):
115 def uid(self, repo_id, commit_id):
110 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
116 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
@@ -119,16 +125,65 b' class RepoFeedView(RepoAppView):'
119 Produce an atom-1.0 feed via feedgenerator module
125 Produce an atom-1.0 feed via feedgenerator module
120 """
126 """
121 self.load_default_context()
127 self.load_default_context()
128 force_recache = self.get_recache_flag()
122
129
123 cache_namespace_uid = 'cache_repo_feed.{}'.format(self.db_repo.repo_id)
130 cache_namespace_uid = 'cache_repo_feed.{}'.format(self.db_repo.repo_id)
124 condition = not self.path_filter.is_enabled
131 condition = not (self.path_filter.is_enabled or force_recache)
125 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
132 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
126
133
127 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
134 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
128 condition=condition)
135 condition=condition)
129 def generate_atom_feed(repo_id, _repo_name, commit_id, _feed_type):
136 def generate_atom_feed(repo_id, _repo_name, _commit_id, _feed_type):
130 feed = Atom1Feed(
137 feed = Atom1Feed(
131 title=self.title % _repo_name,
138 title=self.title % 'atom',
139 link=h.route_url('repo_summary', repo_name=_repo_name),
140 description=self.description % _repo_name,
141 language=self.language,
142 ttl=self.ttl
143 )
144 for commit in reversed(self._get_commits()):
145 date = self._set_timezone(commit.date)
146 feed.add_item(
147 unique_id=self.uid(repo_id, commit.raw_id),
148 title=self._get_title(commit),
149 author_name=commit.author,
150 description=self._get_description(commit),
151 link=h.route_url(
152 'repo_commit', repo_name=_repo_name,
153 commit_id=commit.raw_id),
154 pubdate=date,)
155
156 return feed.content_type, feed.writeString('utf-8')
157
158 commit_id = self.db_repo.changeset_cache.get('raw_id')
159 content_type, feed = generate_atom_feed(
160 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'atom')
161
162 response = Response(feed)
163 response.content_type = content_type
164 return response
165
166 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
167 @HasRepoPermissionAnyDecorator(
168 'repository.read', 'repository.write', 'repository.admin')
169 @view_config(route_name='rss_feed_home', request_method='GET', renderer=None)
170 @view_config(route_name='rss_feed_home_old', request_method='GET', renderer=None)
171 def rss(self):
172 """
173 Produce an rss2 feed via feedgenerator module
174 """
175 self.load_default_context()
176 force_recache = self.get_recache_flag()
177
178 cache_namespace_uid = 'cache_repo_feed.{}'.format(self.db_repo.repo_id)
179 condition = not (self.path_filter.is_enabled or force_recache)
180 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
181
182 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
183 condition=condition)
184 def generate_rss_feed(repo_id, _repo_name, _commit_id, _feed_type):
185 feed = Rss201rev2Feed(
186 title=self.title % 'rss',
132 link=h.route_url('repo_summary', repo_name=_repo_name),
187 link=h.route_url('repo_summary', repo_name=_repo_name),
133 description=self.description % _repo_name,
188 description=self.description % _repo_name,
134 language=self.language,
189 language=self.language,
@@ -146,60 +201,12 b' class RepoFeedView(RepoAppView):'
146 'repo_commit', repo_name=_repo_name,
201 'repo_commit', repo_name=_repo_name,
147 commit_id=commit.raw_id),
202 commit_id=commit.raw_id),
148 pubdate=date,)
203 pubdate=date,)
149
204 return feed.content_type, feed.writeString('utf-8')
150 return feed.mime_type, feed.writeString('utf-8')
151
205
152 commit_id = self.db_repo.changeset_cache.get('raw_id')
206 commit_id = self.db_repo.changeset_cache.get('raw_id')
153 mime_type, feed = generate_atom_feed(
207 content_type, feed = generate_rss_feed(
154 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'atom')
155
156 response = Response(feed)
157 response.content_type = mime_type
158 return response
159
160 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
161 @HasRepoPermissionAnyDecorator(
162 'repository.read', 'repository.write', 'repository.admin')
163 @view_config(route_name='rss_feed_home', request_method='GET', renderer=None)
164 @view_config(route_name='rss_feed_home_old', request_method='GET', renderer=None)
165 def rss(self):
166 """
167 Produce an rss2 feed via feedgenerator module
168 """
169 self.load_default_context()
170
171 cache_namespace_uid = 'cache_repo_feed.{}'.format(self.db_repo.repo_id)
172 condition = not self.path_filter.is_enabled
173 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
174
175 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
176 condition=condition)
177 def generate_rss_feed(repo_id, _repo_name, commit_id, _feed_type):
178 feed = Rss201rev2Feed(
179 title=self.title % _repo_name,
180 link=h.route_url('repo_summary', repo_name=_repo_name),
181 description=self.description % _repo_name,
182 language=self.language,
183 ttl=self.ttl
184 )
185
186 for commit in reversed(self._get_commits()):
187 date = self._set_timezone(commit.date)
188 feed.add_item(
189 unique_id=self.uid(repo_id, commit.raw_id),
190 title=self._get_title(commit),
191 author_name=commit.author,
192 description=self._get_description(commit),
193 link=h.route_url(
194 'repo_commit', repo_name=_repo_name,
195 commit_id=commit.raw_id),
196 pubdate=date,)
197 return feed.mime_type, feed.writeString('utf-8')
198
199 commit_id = self.db_repo.changeset_cache.get('raw_id')
200 mime_type, feed = generate_rss_feed(
201 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'rss')
208 self.db_repo.repo_id, self.db_repo.repo_name, commit_id, 'rss')
202
209
203 response = Response(feed)
210 response = Response(feed)
204 response.content_type = mime_type
211 response.content_type = content_type
205 return response
212 return response
General Comments 0
You need to be logged in to leave comments. Login now