Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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`.
  • Loading branch information...
commit a269ea4fe0a9a7195f1bd8bf5d462f48c226d525 1 parent e1c737b
@Matt-Deacalion Matt-Deacalion authored timgraham committed
View
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
View
12 django/contrib/syndication/views.py
@@ -43,9 +43,9 @@ def __call__(self, request, *args, **kwargs):
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 @@ def get_feed(self, obj, request):
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 @@ def get_feed(self, obj, request):
'item_guid_is_permalink', item),
enclosure = enc,
pubdate = pubdate,
+ updateddate = updateddate,
author_name = author_name,
author_email = author_email,
author_link = author_link,
View
34 django/utils/feedgenerator.py
@@ -114,11 +114,11 @@ def __init__(self, title, link, description, language=None, author_email=None,
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 @@ def add_item(self, title, link, description, author_email=None,
'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 @@ def writeString(self, encoding):
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 @@ def write_items(self, handler):
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
24 docs/ref/contrib/syndication.txt
@@ -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
15 docs/ref/utils.txt
@@ -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
5 docs/releases/1.7.txt
@@ -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
20 tests/syndication/feeds.py
@@ -33,7 +33,10 @@ def item_description(self, item):
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 @@ def item_pubdate(self, item):
# 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
21 tests/syndication/fixtures/feeddata.json
@@ -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
6 tests/syndication/models.py
@@ -5,10 +5,11 @@
@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
78 tests/syndication/tests.py
@@ -58,7 +58,7 @@ def test_rss2_feed(self):
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 @@ def test_rss2_feed(self):
)
# 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 @@ def test_atom_feed(self):
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 @@ def test_custom_feed_generator(self):
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 @@ def test_naive_datetime_conversion(self):
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 @@ def test_aware_datetime_conversion(self):
"""
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
1  tests/syndication/urls.py
@@ -15,6 +15,7 @@
(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()),
Please sign in to comment.
Something went wrong with that request. Please try again.