webhelpers: extracted django feedgenerator as standalone package to replace webhelpers feedgenerator
dan -
r4092:7d375010 default
Not Reviewed
Show More
Add another comment
TODOs: 0 unresolved 0 Resolved
COMMENTS: 0 General 0 Inline
@@ -0,0 +1,21
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
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
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
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
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
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
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
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