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:
parent
e1c737b62f
commit
a269ea4fe0
1
AUTHORS
1
AUTHORS
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
---------
|
||||
|
|
|
@ -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
|
||||
=====================================
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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/')
|
||||
|
|
|
@ -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()),
|
||||
|
|
Loading…
Reference in New Issue