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
Nick Efford <nick@efford.org>
Marc Egli <frog32@me.com>
Matt Deacalion Stevens <matt@dirtymonkey.co.uk>
eibaan@gmail.com
David Eklund
Julia Elman

View File

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

View File

@ -114,11 +114,11 @@ class SyndicationFeed(object):
def add_item(self, title, link, description, author_email=None,
author_name=None, author_link=None, pubdate=None, comments=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
objects except pubdate, which is a datetime.datetime object, and
enclosure, which is an instance of the Enclosure class.
objects except pubdate and updateddate, which are datetime.datetime
objects, and enclosure, which is an instance of the Enclosure class.
"""
to_unicode = lambda s: force_text(s, strings_only=True)
if categories:
@ -134,6 +134,7 @@ class SyndicationFeed(object):
'author_name': to_unicode(author_name),
'author_link': iri_to_uri(author_link),
'pubdate': pubdate,
'updateddate': updateddate,
'comments': to_unicode(comments),
'unique_id': to_unicode(unique_id),
'unique_id_is_permalink': unique_id_is_permalink,
@ -191,15 +192,20 @@ class SyndicationFeed(object):
def latest_post_date(self):
"""
Returns the latest item's pubdate. If none of them have a pubdate,
this returns the current date/time.
Returns the latest item's pubdate or updateddate. If no items
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]
if len(updates) > 0:
updates.sort()
return updates[-1]
else:
return datetime.datetime.now()
latest_date = None
date_keys = ('updateddate', 'pubdate')
for item in self.items:
for date_key in date_keys:
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):
"Represents an RSS enclosure"
@ -349,8 +355,12 @@ class Atom1Feed(SyndicationFeed):
def add_item_elements(self, handler, item):
handler.addQuickElement("title", item['title'])
handler.addQuickElement("link", "", {"href": item['link'], "rel": "alternate"})
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.
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 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
# 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
@ -928,16 +946,22 @@ They share this interface:
* ``categories``
* ``item_copyright``
* ``ttl``
* ``updateddate``
Extra keyword arguments will be stored for `custom feed generators`_.
All parameters, if given, should be Unicode objects, except:
* ``pubdate`` should be a Python :class:`~datetime.datetime` object.
* ``updateddate`` should be a Python :class:`~datetime.datetime` object.
* ``enclosure`` should be an instance of
:class:`django.utils.feedgenerator.Enclosure`.
* ``categories`` should be a sequence of Unicode objects.
.. versionadded:: 1.7
The optional ``updateddate`` argument was added.
:meth:`.SyndicationFeed.write`
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
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``
objects except ``pubdate``, which is a ``datetime.datetime`` object, and
``enclosure``, which is an instance of the ``Enclosure`` class.
objects except ``pubdate`` and ``updateddate``, which are ``datetime.datetime``
objects, and ``enclosure``, which is an instance of the ``Enclosure`` class.
.. versionadded:: 1.7
The optional ``updateddate`` argument was added.
.. method:: num_items()
@ -380,8 +384,9 @@ SyndicationFeed
.. method:: latest_post_date()
Returns the latest item's ``pubdate``. If none of them have a
``pubdate``, this returns the current date/time.
Returns the latest ``pubdate`` or ``updateddate`` for all items in the
feed. If no items have either of these attributes this returns the
current date/time.
Enclosure
---------

View File

@ -67,6 +67,11 @@ Minor features
parameters that are passed to the ``dict`` constructor used to build the new
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
=====================================

View File

@ -33,7 +33,10 @@ class TestRss2Feed(views.Feed):
return "Overridden description: %s" % 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_email = 'test@example.com'
@ -72,6 +75,17 @@ class TestAtomFeed(TestRss2Feed):
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):
"""
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.
"""
def item_pubdate(self, item):
return item.date
return item.published
class TZAwareDatesFeed(TestAtomFeed):
@ -126,7 +140,7 @@ class TZAwareDatesFeed(TestAtomFeed):
# Provide a weird offset so that the test can know it's getting this
# specific offset and not accidentally getting on from
# settings.TIME_ZONE.
return item.date.replace(tzinfo=tzinfo.FixedOffset(42))
return item.published.replace(tzinfo=tzinfo.FixedOffset(42))
class TestFeedUrlFeed(TestAtomFeed):

View File

@ -4,7 +4,8 @@
"pk": 1,
"fields": {
"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,
"fields": {
"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,
"fields": {
"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,
"fields": {
"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
class Entry(models.Model):
title = models.CharField(max_length=200)
date = models.DateTimeField()
updated = models.DateTimeField()
published = models.DateTimeField()
class Meta:
ordering = ('date',)
ordering = ('updated',)
def __str__(self):
return self.title
@ -24,4 +25,3 @@ class Article(models.Model):
def __str__(self):
return self.title

View File

@ -58,7 +58,7 @@ class SyndicationFeedTest(FeedTestCase):
chan = chan_elem[0]
# Find the last build date
d = Entry.objects.latest('date').date
d = Entry.objects.latest('published').published
ltz = tzinfo.LocalTimezone(d)
last_build_date = rfc2822_date(d.replace(tzinfo=ltz))
@ -88,7 +88,7 @@ class SyndicationFeedTest(FeedTestCase):
)
# 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)
pub_date = rfc2822_date(d.replace(tzinfo=ltz))
@ -203,10 +203,61 @@ class SyndicationFeedTest(FeedTestCase):
entries = feed.getElementsByTagName('entry')
self.assertEqual(len(entries), Entry.objects.count())
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]
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):
response = self.client.get('/syndication/custom/')
feed = minidom.parseString(response.content).firstChild
@ -219,7 +270,18 @@ class SyndicationFeedTest(FeedTestCase):
self.assertEqual(len(entries), Entry.objects.count())
for entry in entries:
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]
self.assertEqual(summary.getAttribute('type'), 'html')
@ -245,7 +307,7 @@ class SyndicationFeedTest(FeedTestCase):
doc = minidom.parseString(response.content)
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
d = Entry.objects.latest('date').date
d = Entry.objects.latest('published').published
ltz = tzinfo.LocalTimezone(d)
latest = rfc3339_date(d.replace(tzinfo=ltz))
@ -257,12 +319,12 @@ class SyndicationFeedTest(FeedTestCase):
"""
response = self.client.get('/syndication/aware-dates/')
doc = minidom.parseString(response.content)
updated = doc.getElementsByTagName('updated')[0].firstChild.wholeText
self.assertEqual(updated[-6:], '+00:42')
published = doc.getElementsByTagName('published')[0].firstChild.wholeText
self.assertEqual(published[-6:], '+00:42')
def test_feed_last_modified_time(self):
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
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/no_pubdate/$', feeds.TestNoPubdateFeed()),
(r'^syndication/atom/$', feeds.TestAtomFeed()),
(r'^syndication/latest/$', feeds.TestLatestFeed()),
(r'^syndication/custom/$', feeds.TestCustomFeed()),
(r'^syndication/naive-dates/$', feeds.NaiveDatesFeed()),
(r'^syndication/aware-dates/$', feeds.TZAwareDatesFeed()),