diff options
| author | Adrian Holovaty <adrian@holovaty.com> | 2005-11-12 03:44:53 +0000 |
|---|---|---|
| committer | Adrian Holovaty <adrian@holovaty.com> | 2005-11-12 03:44:53 +0000 |
| commit | 944de9e9e638bc239b03f67f459ad4abe4673e48 (patch) | |
| tree | d144b7c68fc2f0a7169aa07d0a91daa246b94473 /django | |
| parent | e8ae3567393443934d896579dd6d1dfc92bddb95 (diff) | |
Completely refactored legacy RSS framework to the new django.contrib.syndication package. Also added Atom support, changed the way feeds are registered and added documentation for the whole lot. This is backwards-incompatible, but the RSS framework had not yet been documented, so this should only affect tinkerers and WorldOnline. Fixes #329, #498, #502 and #554. Thanks for various patches/ideas to alastair, ismael, hugo, eric moritz and garthk
git-svn-id: http://code.djangoproject.com/svn/django/trunk@1194 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
| -rw-r--r-- | django/conf/urls/rss.py | 6 | ||||
| -rw-r--r-- | django/contrib/syndication/__init__.py | 0 | ||||
| -rw-r--r-- | django/contrib/syndication/feeds.py | 89 | ||||
| -rw-r--r-- | django/contrib/syndication/views.py | 26 | ||||
| -rw-r--r-- | django/core/rss.py | 223 | ||||
| -rw-r--r-- | django/utils/feedgenerator.py | 219 | ||||
| -rw-r--r-- | django/views/rss/rss.py | 12 |
7 files changed, 273 insertions, 302 deletions
diff --git a/django/conf/urls/rss.py b/django/conf/urls/rss.py deleted file mode 100644 index bb6f84e07d..0000000000 --- a/django/conf/urls/rss.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.conf.urls.defaults import * - -urlpatterns = patterns('django.views', - (r'^(?P<slug>[^/]+)/$', 'rss.rss.feed'), - (r'^(?P<slug>[^/]+)/(?P<param>.+)/$', 'rss.rss.feed'), -) diff --git a/django/contrib/syndication/__init__.py b/django/contrib/syndication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/django/contrib/syndication/__init__.py diff --git a/django/contrib/syndication/feeds.py b/django/contrib/syndication/feeds.py new file mode 100644 index 0000000000..c495990c51 --- /dev/null +++ b/django/contrib/syndication/feeds.py @@ -0,0 +1,89 @@ +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.core.template import Context, loader, Template, TemplateDoesNotExist +from django.models.core import sites +from django.utils import feedgenerator +from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE + +def add_domain(domain, url): + if not url.startswith('http://'): + url = u'http://%s%s' % (domain, url) + return url + +class FeedDoesNotExist(ObjectDoesNotExist): + pass + +class Feed: + item_pubdate = None + item_enclosure_url = None + feed_type = feedgenerator.DefaultFeed + + def __init__(self, slug): + self.slug = slug + + def item_link(self, item): + try: + return item.get_absolute_url() + except AttributeError: + raise ImproperlyConfigured, "Give your %s class a get_absolute_url() method, or define an item_link() method in your Feed class." % item.__class__.__name__ + + def __get_dynamic_attr(self, attname, obj): + attr = getattr(self, attname) + if callable(attr): + try: + return attr(obj) + except TypeError: + return attr() + return attr + + def get_feed(self, url=None): + """ + Returns a feedgenerator.DefaultFeed object, fully populated, for + this feed. Raises FeedDoesNotExist for invalid parameters. + """ + if url: + try: + obj = self.get_object(url.split('/')) + except (AttributeError, ObjectDoesNotExist): + raise FeedDoesNotExist + else: + obj = None + + current_site = sites.get_current() + link = self.__get_dynamic_attr('link', obj) + link = add_domain(current_site.domain, link) + + feed = self.feed_type( + title = self.__get_dynamic_attr('title', obj), + link = link, + description = self.__get_dynamic_attr('description', obj), + language = LANGUAGE_CODE.decode() + ) + + try: + title_template = loader.get_template('feeds/%s_title' % self.slug) + except TemplateDoesNotExist: + title_template = Template('{{ obj }}') + try: + description_template = loader.get_template('feeds/%s_description' % self.slug) + except TemplateDoesNotExist: + description_template = Template('{{ obj }}') + + for item in self.__get_dynamic_attr('items', obj): + link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item)) + enc = None + enc_url = self.__get_dynamic_attr('item_enclosure_url', item) + if enc_url: + enc = feedgenerator.Enclosure( + url = enc_url.decode('utf-8'), + length = str(self.__get_dynamic_attr('item_enclosure_length', item)).decode('utf-8'), + mime_type = self.__get_dynamic_attr('item_enclosure_mime_type', item).decode('utf-8'), + ) + feed.add_item( + title = title_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'), + link = link, + description = description_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'), + unique_id = link, + enclosure = enc, + pubdate = self.__get_dynamic_attr('item_pubdate', item), + ) + return feed diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py new file mode 100644 index 0000000000..3236f9dfab --- /dev/null +++ b/django/contrib/syndication/views.py @@ -0,0 +1,26 @@ +from django.contrib.syndication import feeds +from django.core.exceptions import Http404 +from django.utils.httpwrappers import HttpResponse + +def feed(request, url, feed_dict=None): + if not feed_dict: + raise Http404, "No feeds are registered." + + try: + slug, param = url.split('/', 1) + except ValueError: + slug, param = url, '' + + try: + f = feed_dict[slug] + except KeyError: + raise Http404, "Slug %r isn't registered." % slug + + try: + feedgen = f(slug).get_feed(param) + except feeds.FeedDoesNotExist: + raise Http404, "Invalid feed parameters. Slug %r is valid, but other parameters, or lack thereof, are not." % slug + + response = HttpResponse(mimetype='application/xml') + feedgen.write(response, 'utf-8') + return response diff --git a/django/core/rss.py b/django/core/rss.py deleted file mode 100644 index 7ba0c1e30c..0000000000 --- a/django/core/rss.py +++ /dev/null @@ -1,223 +0,0 @@ -from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist -from django.core.template import Context, loader, Template, TemplateDoesNotExist -from django.models.core import sites -from django.utils import feedgenerator -from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE - -def add_domain(domain, url): - if not url.startswith('http://'): - url = u'http://%s%s' % (domain, url) - return url - -class FeedDoesNotExist(ObjectDoesNotExist): - pass - -class Feed: - item_pubdate = None - item_enclosure_url = None - - def item_link(self, item): - try: - return item.get_absolute_url() - except AttributeError: - raise ImproperlyConfigured, "Give your %s class a get_absolute_url() method, or define an item_link() method in your RSS class." % item.__class__.__name__ - - def __get_dynamic_attr(self, attname, obj): - attr = getattr(self, attname) - if callable(attr): - try: - return attr(obj) - except TypeError: - return attr() - return attr - - def get_feed(self, url=None): - """ - Returns a feedgenerator.DefaultRssFeed object, fully populated, for - this feed. Raises FeedDoesNotExist for invalid parameters. - """ - if url: - try: - obj = self.get_object(url.split('/')) - except (AttributeError, ObjectDoesNotExist): - raise FeedDoesNotExist - else: - obj = None - - current_site = sites.get_current() - link = self.__get_dynamic_attr('link', obj) - link = add_domain(current_site.domain, link) - - feed = feedgenerator.DefaultRssFeed( - title = self.__get_dynamic_attr('title', obj), - link = link, - description = self.__get_dynamic_attr('description', obj), - language = LANGUAGE_CODE.decode() - ) - - try: - title_template = loader.get_template('rss/%s_title' % self.slug) - except TemplateDoesNotExist: - title_template = Template('{{ obj }}') - try: - description_template = loader.get_template('rss/%s_description' % self.slug) - except TemplateDoesNotExist: - description_template = Template('{{ obj }}') - - for item in self.__get_dynamic_attr('items', obj): - link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item)) - enc = None - enc_url = self.__get_dynamic_attr('item_enclosure_url', item) - if enc_url: - enc = feedgenerator.Enclosure( - url = enc_url.decode('utf-8'), - length = str(self.__get_dynamic_attr('item_enclosure_length', item)).decode('utf-8'), - mime_type = self.__get_dynamic_attr('item_enclosure_mime_type', item).decode('utf-8'), - ) - feed.add_item( - title = title_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'), - link = link, - description = description_template.render(Context({'obj': item, 'site': current_site})).decode('utf-8'), - unique_id = link, - enclosure = enc, - pubdate = self.__get_dynamic_attr('item_pubdate', item), - ) - return feed - -# DEPRECATED -class FeedConfiguration: - def __init__(self, slug, title_cb, link_cb, description_cb, get_list_func_cb, get_list_kwargs, - param_func=None, param_kwargs_cb=None, get_list_kwargs_cb=None, get_pubdate_cb=None, - enc_url=None, enc_length=None, enc_mime_type=None): - """ - slug -- Normal Python string. Used to register the feed. - - title_cb, link_cb, description_cb -- Functions that take the param - (if applicable) and return a normal Python string. - - get_list_func_cb -- Function that takes the param and returns a - function to use in retrieving items. - - get_list_kwargs -- Dictionary of kwargs to pass to the function - returned by get_list_func_cb. - - param_func -- Function to use in retrieving the param (if applicable). - - param_kwargs_cb -- Function that takes the slug and returns a - dictionary of kwargs to use in param_func. - - get_list_kwargs_cb -- Function that takes the param and returns a - dictionary to use in addition to get_list_kwargs (if applicable). - - get_pubdate_cb -- Function that takes the object and returns a datetime - to use as the publication date in the feed. - - The three enc_* parameters are strings representing methods or - attributes to call on a particular item to get its enclosure - information. Each of those methods/attributes should return a normal - Python string. - """ - self.slug = slug - self.title_cb, self.link_cb = title_cb, link_cb - self.description_cb = description_cb - self.get_list_func_cb = get_list_func_cb - self.get_list_kwargs = get_list_kwargs - self.param_func, self.param_kwargs_cb = param_func, param_kwargs_cb - self.get_list_kwargs_cb = get_list_kwargs_cb - self.get_pubdate_cb = get_pubdate_cb - assert (None == enc_url == enc_length == enc_mime_type) or (enc_url is not None and enc_length is not None and enc_mime_type is not None) - self.enc_url = enc_url - self.enc_length = enc_length - self.enc_mime_type = enc_mime_type - - def get_feed(self, param_slug=None): - """ - Returns a utils.feedgenerator.DefaultRssFeed object, fully populated, - representing this FeedConfiguration. - """ - if param_slug: - try: - param = self.param_func(**self.param_kwargs_cb(param_slug)) - except ObjectDoesNotExist: - raise FeedIsNotRegistered - else: - param = None - current_site = sites.get_current() - f = self._get_feed_generator_object(param) - title_template = loader.get_template('rss/%s_title' % self.slug) - description_template = loader.get_template('rss/%s_description' % self.slug) - kwargs = self.get_list_kwargs.copy() - if param and self.get_list_kwargs_cb: - kwargs.update(self.get_list_kwargs_cb(param)) - get_list_func = self.get_list_func_cb(param) - for obj in get_list_func(**kwargs): - link = obj.get_absolute_url() - if not link.startswith('http://'): - link = u'http://%s%s' % (current_site.domain, link) - enc = None - if self.enc_url: - enc_url = getattr(obj, self.enc_url) - enc_length = getattr(obj, self.enc_length) - enc_mime_type = getattr(obj, self.enc_mime_type) - try: - enc_url = enc_url() - except TypeError: - pass - try: - enc_length = enc_length() - except TypeError: - pass - try: - enc_mime_type = enc_mime_type() - except TypeError: - pass - enc = feedgenerator.Enclosure(enc_url.decode('utf-8'), - (enc_length and str(enc_length).decode('utf-8') or ''), enc_mime_type.decode('utf-8')) - f.add_item( - title = title_template.render(Context({'obj': obj, 'site': current_site})).decode('utf-8'), - link = link, - description = description_template.render(Context({'obj': obj, 'site': current_site})).decode('utf-8'), - unique_id=link, - enclosure=enc, - pubdate = self.get_pubdate_cb and self.get_pubdate_cb(obj) or None, - ) - return f - - def _get_feed_generator_object(self, param): - current_site = sites.get_current() - link = self.link_cb(param).decode() - if not link.startswith('http://'): - link = u'http://%s%s' % (current_site.domain, link) - return feedgenerator.DefaultRssFeed( - title = self.title_cb(param).decode(), - link = link, - description = self.description_cb(param).decode(), - language = LANGUAGE_CODE.decode(), - ) - - -# global dict used by register_feed and get_registered_feed -_registered_feeds = {} - -# DEPRECATED -class FeedIsNotRegistered(Exception): - pass - -# DEPRECATED -def register_feed(feed): - _registered_feeds[feed.slug] = feed - -def register_feeds(*feeds): - for f in feeds: - _registered_feeds[f.slug] = f - -def get_registered_feed(slug): - # try to load a RSS settings module so that feeds can be registered - try: - __import__(SETTINGS_MODULE + '_rss', '', '', ['']) - except (KeyError, ImportError, ValueError): - pass - try: - return _registered_feeds[slug] - except KeyError: - raise FeedIsNotRegistered diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index e8e58718ff..d482685383 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -19,21 +19,42 @@ http://diveintomark.org/archives/2004/02/04/incompatible-rss """ from django.utils.xmlutils import SimplerXMLGenerator +import datetime, re, time +import email.Utils +from xml.dom import minidom +from xml.parsers.expat import ExpatError + +def rfc2822_date(date): + return email.Utils.formatdate(time.mktime(date.timetuple())) + +def get_tag_uri(url, date): + "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id" + tag = re.sub('^http://', '', url) + if date is not None: + tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1) + tag = re.sub('#', '/', tag) + return 'tag:' + tag class SyndicationFeed: "Base class for all syndication feeds. Subclasses should provide write()" - def __init__(self, title, link, description, language=None): - self.feed_info = { + def __init__(self, title, link, description, language=None, author_email=None, + author_name=None, author_link=None, subtitle=None, categories=None): + self.feed = { 'title': title, 'link': link, 'description': description, 'language': language, + 'author_email': author_email, + 'author_name': author_name, + 'author_link': author_link, + 'subtitle': subtitle, + 'categories': categories or (), } self.items = [] def add_item(self, title, link, description, author_email=None, - author_name=None, pubdate=None, comments=None, unique_id=None, - enclosure=None, categories=None): + author_name=None, pubdate=None, comments=None, + unique_id=None, enclosure=None, categories=()): """ Adds an item to the feed. All args are expected to be Python Unicode objects except pubdate, which is a datetime.datetime object, and @@ -49,7 +70,7 @@ class SyndicationFeed: 'comments': comments, 'unique_id': unique_id, 'enclosure': enclosure, - 'categories': categories or [], + 'categories': categories or (), }) def num_items(self): @@ -71,6 +92,18 @@ class SyndicationFeed: self.write(s, encoding) return s.getvalue() + def latest_post_date(self): + """ + Returns the latest item's pubdate. If none of them have a pubdate, + this returns the current date/time. + """ + updates = [i['pubdate'] for i in self.items if i['pubdate'] is not None] + if len(updates) > 0: + updates.sort() + return updates[-1] + else: + return datetime.datetime.now() + class Enclosure: "Represents an RSS enclosure" def __init__(self, url, length, mime_type): @@ -81,72 +114,136 @@ class RssFeed(SyndicationFeed): def write(self, outfile, encoding): handler = SimplerXMLGenerator(outfile, encoding) handler.startDocument() - self.writeRssElement(handler) - self.writeChannelElement(handler) - for item in self.items: - self.writeRssItem(handler, item) + handler.startElement(u"rss", {u"version": self._version}) + handler.startElement(u"channel", {}) + handler.addQuickElement(u"title", self.feed['title']) + handler.addQuickElement(u"link", self.feed['link']) + handler.addQuickElement(u"description", self.feed['description']) + if self.feed['language'] is not None: + handler.addQuickElement(u"language", self.feed['language']) + self.write_items(handler) self.endChannelElement(handler) - self.endRssElement(handler) - - def writeRssElement(self, handler): - "Adds the <rss> element to handler, taking care of versioning, etc." - raise NotImplementedError - - def endRssElement(self, handler): - "Ends the <rss> element." handler.endElement(u"rss") - def writeChannelElement(self, handler): - handler.startElement(u"channel", {}) - handler.addQuickElement(u"title", self.feed_info['title'], {}) - handler.addQuickElement(u"link", self.feed_info['link'], {}) - handler.addQuickElement(u"description", self.feed_info['description'], {}) - if self.feed_info['language'] is not None: - handler.addQuickElement(u"language", self.feed_info['language'], {}) - def endChannelElement(self, handler): handler.endElement(u"channel") class RssUserland091Feed(RssFeed): - def writeRssElement(self, handler): - handler.startElement(u"rss", {u"version": u"0.91"}) - - def writeRssItem(self, handler, item): - handler.startElement(u"item", {}) - handler.addQuickElement(u"title", item['title'], {}) - handler.addQuickElement(u"link", item['link'], {}) - if item['description'] is not None: - handler.addQuickElement(u"description", item['description'], {}) - handler.endElement(u"item") + _version = u"0.91" + def write_items(self, handler): + for item in self.items: + handler.startElement(u"item", {}) + handler.addQuickElement(u"title", item['title']) + handler.addQuickElement(u"link", item['link']) + if item['description'] is not None: + handler.addQuickElement(u"description", item['description']) + handler.endElement(u"item") class Rss201rev2Feed(RssFeed): # Spec: http://blogs.law.harvard.edu/tech/rss - def writeRssElement(self, handler): - handler.startElement(u"rss", {u"version": u"2.0"}) + _version = u"2.0" + def write_items(self, handler): + for item in self.items: + handler.startElement(u"item", {}) + handler.addQuickElement(u"title", item['title']) + handler.addQuickElement(u"link", item['link']) + if item['description'] is not None: + handler.addQuickElement(u"description", item['description']) + + # Author information. + if item['author_email'] is not None and item['author_name'] is not None: + handler.addQuickElement(u"author", u"%s (%s)" % \ + (item['author_email'], item['author_name'])) + + if item['pubdate'] is not None: + handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('ascii')) + if item['comments'] is not None: + handler.addQuickElement(u"comments", item['comments']) + if item['unique_id'] is not None: + handler.addQuickElement(u"guid", item['unique_id']) + + # Enclosure. + if item['enclosure'] is not None: + handler.addQuickElement(u"enclosure", '', + {u"url": item['enclosure'].url, u"length": item['enclosure'].length, + u"type": item['enclosure'].mime_type}) + + # Categories. + for cat in item['categories']: + handler.addQuickElement(u"category", cat) + + handler.endElement(u"item") + +class Atom1Feed(SyndicationFeed): + # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html + ns = u"http://www.w3.org/2005/Atom" + def write(self, outfile, encoding): + handler = SimplerXMLGenerator(outfile, encoding) + handler.startDocument() + if self.feed['language'] is not None: + handler.startElement(u"feed", {u"xmlns": self.ns, u"xml:lang": self.feed['language']}) + else: + handler.startElement(u"feed", {u"xmlns": self.ns}) + handler.addQuickElement(u"title", self.feed['title']) + handler.addQuickElement(u"link", "", {u"href": self.feed['link']}) + handler.addQuickElement(u"id", self.feed['link']) + handler.addQuickElement(u"updated", rfc2822_date(self.latest_post_date()).decode('ascii')) + if self.feed['author_name'] is not None: + handler.startElement(u"author", {}) + handler.addQuickElement(u"name", self.feed['author_name']) + if self.feed['author_email'] is not None: + handler.addQuickElement(u"email", self.feed['author_email']) + if self.feed['author_link'] is not None: + handler.addQuickElement(u"uri", self.feed['author_link']) + handler.endElement(u"author") + if self.feed['subtitle'] is not None: + handler.addQuickElement(u"subtitle", self.feed['subtitle']) + for cat in self.feed['categories']: + handler.addQuickElement(u"category", "", {u"term": cat}) + self.write_items(handler) + handler.endElement(u"feed") + + def write_items(self, handler): + for item in self.items: + handler.startElement(u"entry", {}) + handler.addQuickElement(u"title", item['title']) + handler.addQuickElement(u"link", item['link']) + if item['pubdate'] is not None: + handler.addQuickElement(u"updated", rfc2822_date(item['pubdate']).decode('ascii')) + + # Author information. + if item['author_name'] is not None: + handler.startElement(u"author", {}) + handler.addQuickElement(u"name", item['author_name']) + if item['author_email'] is not None: + handler.addQuickElement(u"email", item['author_email']) + handler.endElement(u"author") + + # Unique ID. + if item['unique_id'] is not None: + unique_id = item['unique_id'] + else: + unique_id = get_tag_uri(item['link'], item['pubdate']) + handler.addQuickElement(u"id", unique_id) + + # Summary. + if item['description'] is not None: + handler.addQuickElement(u"summary", item['description'], {u"type": u"html"}) + + # Enclosure. + if item['enclosure'] is not None: + handler.addQuickElement(u"link", '', + {u"rel": u"enclosure", + u"href": item['enclosure'].url, + u"length": item['enclosure'].length, + u"type": item['enclosure'].mime_type}) + + # Categories: + for cat in item['categories']: + handler.addQuickElement(u"category", u"", {u"term": cat}) - def writeRssItem(self, handler, item): - handler.startElement(u"item", {}) - handler.addQuickElement(u"title", item['title'], {}) - handler.addQuickElement(u"link", item['link'], {}) - if item['description'] is not None: - handler.addQuickElement(u"description", item['description'], {}) - if item['author_email'] is not None and item['author_name'] is not None: - handler.addQuickElement(u"author", u"%s (%s)" % \ - (item['author_email'], item['author_name']), {}) - if item['pubdate'] is not None: - handler.addQuickElement(u"pubDate", item['pubdate'].strftime('%a, %d %b %Y %H:%M:%S %Z'), {}) - if item['comments'] is not None: - handler.addQuickElement(u"comments", item['comments'], {}) - if item['unique_id'] is not None: - handler.addQuickElement(u"guid", item['unique_id'], {}) - if item['enclosure'] is not None: - handler.addQuickElement(u"enclosure", '', - {u"url": item['enclosure'].url, u"length": item['enclosure'].length, - u"type": item['enclosure'].mime_type}) - for cat in item['categories']: - handler.addQuickElement(u"category", cat, {}) - handler.endElement(u"item") + handler.endElement(u"entry") # This isolates the decision of what the system default is, so calling code can -# do "feedgenerator.DefaultRssFeed" instead of "feedgenerator.Rss201rev2Feed". -DefaultRssFeed = Rss201rev2Feed +# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed". +DefaultFeed = Rss201rev2Feed diff --git a/django/views/rss/rss.py b/django/views/rss/rss.py deleted file mode 100644 index 97c2f22dc4..0000000000 --- a/django/views/rss/rss.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.core import rss -from django.core.exceptions import Http404 -from django.utils.httpwrappers import HttpResponse - -def feed(request, slug, param=None): - try: - f = rss.get_registered_feed(slug).get_feed(param) - except (rss.FeedIsNotRegistered, rss.FeedDoesNotExist): - raise Http404 - response = HttpResponse(mimetype='application/xml') - f.write(response, 'utf-8') - return response |
