Browse Source

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`.
Matt Deacalion Stevens 11 years ago
parent
commit
a269ea4fe0

+ 1 - 0
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

+ 9 - 3
django/contrib/syndication/views.py

@@ -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,

+ 22 - 12
django/utils/feedgenerator.py

@@ -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:

+ 24 - 0
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.
 

+ 10 - 5
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
 ---------

+ 5 - 0
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
 =====================================
 

+ 17 - 3
tests/syndication/feeds.py

@@ -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):

+ 17 - 4
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"
     }
   },
   {

+ 3 - 3
tests/syndication/models.py

@@ -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
-

+ 70 - 8
tests/syndication/tests.py

@@ -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/')

+ 1 - 0
tests/syndication/urls.py

@@ -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()),