Fixed #14656 -- Added Atom1Feed `published` element

Some feed aggregators make use of the `published` element as well as
the `updated` element (within the Atom standard -- http://bit.ly/2YySb).

The standard allows for these two elements to be present in the same
entry. `Atom1Feed` had implemented the `updated` element which was
incorrectly taking the date from `pubdate`.
This commit is contained in:
Matt Deacalion Stevens 2013-07-17 20:20:20 +01:00 committed by Tim Graham
parent e1c737b62f
commit a269ea4fe0
11 changed files with 179 additions and 38 deletions

View File

@ -204,6 +204,7 @@ answer newbie questions, and generally made Django that much better:
Clint Ecker Clint Ecker
Nick Efford <nick@efford.org> Nick Efford <nick@efford.org>
Marc Egli <frog32@me.com> Marc Egli <frog32@me.com>
Matt Deacalion Stevens <matt@dirtymonkey.co.uk>
eibaan@gmail.com eibaan@gmail.com
David Eklund David Eklund
Julia Elman Julia Elman

View File

@ -43,9 +43,9 @@ class Feed(object):
raise Http404('Feed object does not exist.') raise Http404('Feed object does not exist.')
feedgen = self.get_feed(obj, request) feedgen = self.get_feed(obj, request)
response = HttpResponse(content_type=feedgen.mime_type) response = HttpResponse(content_type=feedgen.mime_type)
if hasattr(self, 'item_pubdate'): if hasattr(self, 'item_pubdate') or hasattr(self, 'item_updateddate'):
# if item_pubdate is defined for the feed, set header so as # if item_pubdate or item_updateddate is defined for the feed, set
# ConditionalGetMiddleware is able to send 304 NOT MODIFIED # header so as ConditionalGetMiddleware is able to send 304 NOT MODIFIED
response['Last-Modified'] = http_date( response['Last-Modified'] = http_date(
timegm(feedgen.latest_post_date().utctimetuple())) timegm(feedgen.latest_post_date().utctimetuple()))
feedgen.write(response, 'utf-8') feedgen.write(response, 'utf-8')
@ -191,6 +191,11 @@ class Feed(object):
ltz = tzinfo.LocalTimezone(pubdate) ltz = tzinfo.LocalTimezone(pubdate)
pubdate = pubdate.replace(tzinfo=ltz) pubdate = pubdate.replace(tzinfo=ltz)
updateddate = self.__get_dynamic_attr('item_updateddate', item)
if updateddate and is_naive(updateddate):
ltz = tzinfo.LocalTimezone(updateddate)
updateddate = updateddate.replace(tzinfo=ltz)
feed.add_item( feed.add_item(
title = title, title = title,
link = link, link = link,
@ -200,6 +205,7 @@ class Feed(object):
'item_guid_is_permalink', item), 'item_guid_is_permalink', item),
enclosure = enc, enclosure = enc,
pubdate = pubdate, pubdate = pubdate,
updateddate = updateddate,
author_name = author_name, author_name = author_name,
author_email = author_email, author_email = author_email,
author_link = author_link, author_link = author_link,

View File

@ -114,11 +114,11 @@ class SyndicationFeed(object):
def add_item(self, title, link, description, author_email=None, def add_item(self, title, link, description, author_email=None,
author_name=None, author_link=None, pubdate=None, comments=None, author_name=None, author_link=None, pubdate=None, comments=None,
unique_id=None, unique_id_is_permalink=None, enclosure=None, unique_id=None, unique_id_is_permalink=None, enclosure=None,
categories=(), item_copyright=None, ttl=None, **kwargs): categories=(), item_copyright=None, ttl=None, updateddate=None, **kwargs):
""" """
Adds an item to the feed. All args are expected to be Python Unicode Adds an item to the feed. All args are expected to be Python Unicode
objects except pubdate, which is a datetime.datetime object, and objects except pubdate and updateddate, which are datetime.datetime
enclosure, which is an instance of the Enclosure class. objects, and enclosure, which is an instance of the Enclosure class.
""" """
to_unicode = lambda s: force_text(s, strings_only=True) to_unicode = lambda s: force_text(s, strings_only=True)
if categories: if categories:
@ -134,6 +134,7 @@ class SyndicationFeed(object):
'author_name': to_unicode(author_name), 'author_name': to_unicode(author_name),
'author_link': iri_to_uri(author_link), 'author_link': iri_to_uri(author_link),
'pubdate': pubdate, 'pubdate': pubdate,
'updateddate': updateddate,
'comments': to_unicode(comments), 'comments': to_unicode(comments),
'unique_id': to_unicode(unique_id), 'unique_id': to_unicode(unique_id),
'unique_id_is_permalink': unique_id_is_permalink, 'unique_id_is_permalink': unique_id_is_permalink,
@ -191,15 +192,20 @@ class SyndicationFeed(object):
def latest_post_date(self): def latest_post_date(self):
""" """
Returns the latest item's pubdate. If none of them have a pubdate, Returns the latest item's pubdate or updateddate. If no items
this returns the current date/time. have either of these attributes this returns the current date/time.
""" """
updates = [i['pubdate'] for i in self.items if i['pubdate'] is not None] latest_date = None
if len(updates) > 0: date_keys = ('updateddate', 'pubdate')
updates.sort()
return updates[-1] for item in self.items:
else: for date_key in date_keys:
return datetime.datetime.now() item_date = item.get(date_key)
if item_date:
if latest_date is None or item_date > latest_date:
latest_date = item_date
return latest_date or datetime.datetime.now()
class Enclosure(object): class Enclosure(object):
"Represents an RSS enclosure" "Represents an RSS enclosure"
@ -349,8 +355,12 @@ class Atom1Feed(SyndicationFeed):
def add_item_elements(self, handler, item): def add_item_elements(self, handler, item):
handler.addQuickElement("title", item['title']) handler.addQuickElement("title", item['title'])
handler.addQuickElement("link", "", {"href": item['link'], "rel": "alternate"}) handler.addQuickElement("link", "", {"href": item['link'], "rel": "alternate"})
if item['pubdate'] is not None: if item['pubdate'] is not None:
handler.addQuickElement("updated", rfc3339_date(item['pubdate'])) handler.addQuickElement('published', rfc3339_date(item['pubdate']))
if item['updateddate'] is not None:
handler.addQuickElement('updated', rfc3339_date(item['updateddate']))
# Author information. # Author information.
if item['author_name'] is not None: if item['author_name'] is not None:

View File

@ -815,6 +815,24 @@ This example illustrates all possible attributes and methods for a
item_pubdate = datetime.datetime(2005, 5, 3) # Hard-coded pubdate. item_pubdate = datetime.datetime(2005, 5, 3) # Hard-coded pubdate.
# ITEM UPDATED -- It's optional to use one of these three. This is a
# hook that specifies how to get the updateddate for a given item.
# In each case, the method/attribute should return a Python
# datetime.datetime object.
def item_updateddate(self, item):
"""
Takes an item, as returned by items(), and returns the item's
updateddate.
"""
def item_updateddate(self):
"""
Returns the updateddated for every item in the feed.
"""
item_updateddate = datetime.datetime(2005, 5, 3) # Hard-coded updateddate.
# ITEM CATEGORIES -- It's optional to use one of these three. This is # ITEM CATEGORIES -- It's optional to use one of these three. This is
# a hook that specifies how to get the list of categories for a given # a hook that specifies how to get the list of categories for a given
# item. In each case, the method/attribute should return an iterable # item. In each case, the method/attribute should return an iterable
@ -928,16 +946,22 @@ They share this interface:
* ``categories`` * ``categories``
* ``item_copyright`` * ``item_copyright``
* ``ttl`` * ``ttl``
* ``updateddate``
Extra keyword arguments will be stored for `custom feed generators`_. Extra keyword arguments will be stored for `custom feed generators`_.
All parameters, if given, should be Unicode objects, except: All parameters, if given, should be Unicode objects, except:
* ``pubdate`` should be a Python :class:`~datetime.datetime` object. * ``pubdate`` should be a Python :class:`~datetime.datetime` object.
* ``updateddate`` should be a Python :class:`~datetime.datetime` object.
* ``enclosure`` should be an instance of * ``enclosure`` should be an instance of
:class:`django.utils.feedgenerator.Enclosure`. :class:`django.utils.feedgenerator.Enclosure`.
* ``categories`` should be a sequence of Unicode objects. * ``categories`` should be a sequence of Unicode objects.
.. versionadded:: 1.7
The optional ``updateddate`` argument was added.
:meth:`.SyndicationFeed.write` :meth:`.SyndicationFeed.write`
Outputs the feed in the given encoding to outfile, which is a file-like object. Outputs the feed in the given encoding to outfile, which is a file-like object.

View File

@ -342,11 +342,15 @@ SyndicationFeed
All parameters should be Unicode objects, except ``categories``, which All parameters should be Unicode objects, except ``categories``, which
should be a sequence of Unicode objects. should be a sequence of Unicode objects.
.. method:: add_item(title, link, description, [author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, unique_id=None, enclosure=None, categories=(), item_copyright=None, ttl=None, **kwargs]) .. method:: add_item(title, link, description, [author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, unique_id=None, enclosure=None, categories=(), item_copyright=None, ttl=None, updateddate=None, **kwargs])
Adds an item to the feed. All args are expected to be Python ``unicode`` Adds an item to the feed. All args are expected to be Python ``unicode``
objects except ``pubdate``, which is a ``datetime.datetime`` object, and objects except ``pubdate`` and ``updateddate``, which are ``datetime.datetime``
``enclosure``, which is an instance of the ``Enclosure`` class. objects, and ``enclosure``, which is an instance of the ``Enclosure`` class.
.. versionadded:: 1.7
The optional ``updateddate`` argument was added.
.. method:: num_items() .. method:: num_items()
@ -380,8 +384,9 @@ SyndicationFeed
.. method:: latest_post_date() .. method:: latest_post_date()
Returns the latest item's ``pubdate``. If none of them have a Returns the latest ``pubdate`` or ``updateddate`` for all items in the
``pubdate``, this returns the current date/time. feed. If no items have either of these attributes this returns the
current date/time.
Enclosure Enclosure
--------- ---------

View File

@ -67,6 +67,11 @@ Minor features
parameters that are passed to the ``dict`` constructor used to build the new parameters that are passed to the ``dict`` constructor used to build the new
context level. context level.
* The :class:`~django.utils.feedgenerator.Atom1Feed` syndication feed's
``updated`` element now utilizes `updateddate` instead of ``pubdate``,
allowing the ``published`` element to be included in the feed (which
relies on ``pubdate``).
Backwards incompatible changes in 1.7 Backwards incompatible changes in 1.7
===================================== =====================================

View File

@ -33,7 +33,10 @@ class TestRss2Feed(views.Feed):
return "Overridden description: %s" % item return "Overridden description: %s" % item
def item_pubdate(self, item): def item_pubdate(self, item):
return item.date return item.published
def item_updateddate(self, item):
return item.updated
item_author_name = 'Sally Smith' item_author_name = 'Sally Smith'
item_author_email = 'test@example.com' item_author_email = 'test@example.com'
@ -72,6 +75,17 @@ class TestAtomFeed(TestRss2Feed):
subtitle = TestRss2Feed.description subtitle = TestRss2Feed.description
class TestLatestFeed(TestRss2Feed):
"""
A feed where the latest entry date is an `updated` element.
"""
feed_type = feedgenerator.Atom1Feed
subtitle = TestRss2Feed.description
def items(self):
return Entry.objects.exclude(pk=5)
class ArticlesFeed(TestRss2Feed): class ArticlesFeed(TestRss2Feed):
""" """
A feed to test no link being defined. Articles have no get_absolute_url() A feed to test no link being defined. Articles have no get_absolute_url()
@ -115,7 +129,7 @@ class NaiveDatesFeed(TestAtomFeed):
A feed with naive (non-timezone-aware) dates. A feed with naive (non-timezone-aware) dates.
""" """
def item_pubdate(self, item): def item_pubdate(self, item):
return item.date return item.published
class TZAwareDatesFeed(TestAtomFeed): class TZAwareDatesFeed(TestAtomFeed):
@ -126,7 +140,7 @@ class TZAwareDatesFeed(TestAtomFeed):
# Provide a weird offset so that the test can know it's getting this # Provide a weird offset so that the test can know it's getting this
# specific offset and not accidentally getting on from # specific offset and not accidentally getting on from
# settings.TIME_ZONE. # settings.TIME_ZONE.
return item.date.replace(tzinfo=tzinfo.FixedOffset(42)) return item.published.replace(tzinfo=tzinfo.FixedOffset(42))
class TestFeedUrlFeed(TestAtomFeed): class TestFeedUrlFeed(TestAtomFeed):

View File

@ -4,7 +4,8 @@
"pk": 1, "pk": 1,
"fields": { "fields": {
"title": "My first entry", "title": "My first entry",
"date": "1850-01-01 12:30:00" "updated": "1850-01-01 12:30:00",
"published": "1066-09-25 20:15:00"
} }
}, },
{ {
@ -12,7 +13,8 @@
"pk": 2, "pk": 2,
"fields": { "fields": {
"title": "My second entry", "title": "My second entry",
"date": "2008-01-02 12:30:00" "updated": "2008-01-02 12:30:00",
"published": "2006-03-17 18:00:00"
} }
}, },
{ {
@ -20,7 +22,8 @@
"pk": 3, "pk": 3,
"fields": { "fields": {
"title": "My third entry", "title": "My third entry",
"date": "2008-01-02 13:30:00" "updated": "2008-01-02 13:30:00",
"published": "2005-06-14 10:45:00"
} }
}, },
{ {
@ -28,7 +31,17 @@
"pk": 4, "pk": 4,
"fields": { "fields": {
"title": "A & B < C > D", "title": "A & B < C > D",
"date": "2008-01-03 13:30:00" "updated": "2008-01-03 13:30:00",
"published": "2005-11-25 12:11:23"
}
},
{
"model": "syndication.entry",
"pk": 5,
"fields": {
"title": "My last entry",
"updated": "2013-01-20 00:00:00",
"published": "2013-03-25 20:00:00"
} }
}, },
{ {

View File

@ -5,10 +5,11 @@ from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible @python_2_unicode_compatible
class Entry(models.Model): class Entry(models.Model):
title = models.CharField(max_length=200) title = models.CharField(max_length=200)
date = models.DateTimeField() updated = models.DateTimeField()
published = models.DateTimeField()
class Meta: class Meta:
ordering = ('date',) ordering = ('updated',)
def __str__(self): def __str__(self):
return self.title return self.title
@ -24,4 +25,3 @@ class Article(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title

View File

@ -58,7 +58,7 @@ class SyndicationFeedTest(FeedTestCase):
chan = chan_elem[0] chan = chan_elem[0]
# Find the last build date # Find the last build date
d = Entry.objects.latest('date').date d = Entry.objects.latest('published').published
ltz = tzinfo.LocalTimezone(d) ltz = tzinfo.LocalTimezone(d)
last_build_date = rfc2822_date(d.replace(tzinfo=ltz)) last_build_date = rfc2822_date(d.replace(tzinfo=ltz))
@ -88,7 +88,7 @@ class SyndicationFeedTest(FeedTestCase):
) )
# Find the pubdate of the first feed item # Find the pubdate of the first feed item
d = Entry.objects.get(pk=1).date d = Entry.objects.get(pk=1).published
ltz = tzinfo.LocalTimezone(d) ltz = tzinfo.LocalTimezone(d)
pub_date = rfc2822_date(d.replace(tzinfo=ltz)) pub_date = rfc2822_date(d.replace(tzinfo=ltz))
@ -203,10 +203,61 @@ class SyndicationFeedTest(FeedTestCase):
entries = feed.getElementsByTagName('entry') entries = feed.getElementsByTagName('entry')
self.assertEqual(len(entries), Entry.objects.count()) self.assertEqual(len(entries), Entry.objects.count())
for entry in entries: for entry in entries:
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'category', 'updated', 'rights', 'author']) self.assertChildNodes(entry, [
'title',
'link',
'id',
'summary',
'category',
'updated',
'published',
'rights',
'author',
])
summary = entry.getElementsByTagName('summary')[0] summary = entry.getElementsByTagName('summary')[0]
self.assertEqual(summary.getAttribute('type'), 'html') self.assertEqual(summary.getAttribute('type'), 'html')
def test_atom_feed_published_and_updated_elements(self):
"""
Test that the published and updated elements are not
the same and now adhere to RFC 4287.
"""
response = self.client.get('/syndication/atom/')
feed = minidom.parseString(response.content).firstChild
entries = feed.getElementsByTagName('entry')
published = entries[0].getElementsByTagName('published')[0].firstChild.wholeText
updated = entries[0].getElementsByTagName('updated')[0].firstChild.wholeText
self.assertNotEqual(published, updated)
def test_latest_post_date(self):
"""
Test that both the published and updated dates are
considered when determining the latest post date.
"""
# this feed has a `published` element with the latest date
response = self.client.get('/syndication/atom/')
feed = minidom.parseString(response.content).firstChild
updated = feed.getElementsByTagName('updated')[0].firstChild.wholeText
d = Entry.objects.latest('published').published
ltz = tzinfo.LocalTimezone(d)
latest_published = rfc3339_date(d.replace(tzinfo=ltz))
self.assertEqual(updated, latest_published)
# this feed has an `updated` element with the latest date
response = self.client.get('/syndication/latest/')
feed = minidom.parseString(response.content).firstChild
updated = feed.getElementsByTagName('updated')[0].firstChild.wholeText
d = Entry.objects.exclude(pk=5).latest('updated').updated
ltz = tzinfo.LocalTimezone(d)
latest_updated = rfc3339_date(d.replace(tzinfo=ltz))
self.assertEqual(updated, latest_updated)
def test_custom_feed_generator(self): def test_custom_feed_generator(self):
response = self.client.get('/syndication/custom/') response = self.client.get('/syndication/custom/')
feed = minidom.parseString(response.content).firstChild feed = minidom.parseString(response.content).firstChild
@ -219,7 +270,18 @@ class SyndicationFeedTest(FeedTestCase):
self.assertEqual(len(entries), Entry.objects.count()) self.assertEqual(len(entries), Entry.objects.count())
for entry in entries: for entry in entries:
self.assertEqual(entry.getAttribute('bacon'), 'yum') self.assertEqual(entry.getAttribute('bacon'), 'yum')
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'ministry', 'rights', 'author', 'updated', 'category']) self.assertChildNodes(entry, [
'title',
'link',
'id',
'summary',
'ministry',
'rights',
'author',
'updated',
'published',
'category',
])
summary = entry.getElementsByTagName('summary')[0] summary = entry.getElementsByTagName('summary')[0]
self.assertEqual(summary.getAttribute('type'), 'html') self.assertEqual(summary.getAttribute('type'), 'html')
@ -245,7 +307,7 @@ class SyndicationFeedTest(FeedTestCase):
doc = minidom.parseString(response.content) doc = minidom.parseString(response.content)
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
d = Entry.objects.latest('date').date d = Entry.objects.latest('published').published
ltz = tzinfo.LocalTimezone(d) ltz = tzinfo.LocalTimezone(d)
latest = rfc3339_date(d.replace(tzinfo=ltz)) latest = rfc3339_date(d.replace(tzinfo=ltz))
@ -257,12 +319,12 @@ class SyndicationFeedTest(FeedTestCase):
""" """
response = self.client.get('/syndication/aware-dates/') response = self.client.get('/syndication/aware-dates/')
doc = minidom.parseString(response.content) doc = minidom.parseString(response.content)
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText published = doc.getElementsByTagName('published')[0].firstChild.wholeText
self.assertEqual(updated[-6:], '+00:42') self.assertEqual(published[-6:], '+00:42')
def test_feed_last_modified_time(self): def test_feed_last_modified_time(self):
response = self.client.get('/syndication/naive-dates/') response = self.client.get('/syndication/naive-dates/')
self.assertEqual(response['Last-Modified'], 'Thu, 03 Jan 2008 19:30:00 GMT') self.assertEqual(response['Last-Modified'], 'Tue, 26 Mar 2013 01:00:00 GMT')
# No last-modified when feed has no item_pubdate # No last-modified when feed has no item_pubdate
response = self.client.get('/syndication/no_pubdate/') response = self.client.get('/syndication/no_pubdate/')

View File

@ -15,6 +15,7 @@ urlpatterns = patterns('django.contrib.syndication.views',
(r'^syndication/rss091/$', feeds.TestRss091Feed()), (r'^syndication/rss091/$', feeds.TestRss091Feed()),
(r'^syndication/no_pubdate/$', feeds.TestNoPubdateFeed()), (r'^syndication/no_pubdate/$', feeds.TestNoPubdateFeed()),
(r'^syndication/atom/$', feeds.TestAtomFeed()), (r'^syndication/atom/$', feeds.TestAtomFeed()),
(r'^syndication/latest/$', feeds.TestLatestFeed()),
(r'^syndication/custom/$', feeds.TestCustomFeed()), (r'^syndication/custom/$', feeds.TestCustomFeed()),
(r'^syndication/naive-dates/$', feeds.NaiveDatesFeed()), (r'^syndication/naive-dates/$', feeds.NaiveDatesFeed()),
(r'^syndication/aware-dates/$', feeds.TZAwareDatesFeed()), (r'^syndication/aware-dates/$', feeds.TZAwareDatesFeed()),