From ca61195827efdf6ec54e8280e4cfd9a34f07b8b4 Mon Sep 17 00:00:00 2001
From: Artem Rizhov <artem.rizhov@gmail.com>
Date: Wed, 8 Oct 2014 18:27:17 +0300
Subject: [PATCH] Fixed #23555 -- Avoided suppressing IndexError in
 QuerySet.first() and .last()

---
 django/db/models/query.py              | 18 ++++++++----------
 tests/get_earliest_or_latest/models.py | 15 +++++++++++++++
 tests/get_earliest_or_latest/tests.py  | 26 +++++++++++++++++++++++++-
 3 files changed, 48 insertions(+), 11 deletions(-)

diff --git a/django/db/models/query.py b/django/db/models/query.py
index be5892a371..9371aa14e0 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -516,21 +516,19 @@ class QuerySet(object):
         """
         Returns the first object of a query, returns None if no match is found.
         """
-        qs = self if self.ordered else self.order_by('pk')
-        try:
-            return qs[0]
-        except IndexError:
-            return None
+        objects = list((self if self.ordered else self.order_by('pk'))[:1])
+        if objects:
+            return objects[0]
+        return None
 
     def last(self):
         """
         Returns the last object of a query, returns None if no match is found.
         """
-        qs = self.reverse() if self.ordered else self.order_by('-pk')
-        try:
-            return qs[0]
-        except IndexError:
-            return None
+        objects = list((self.reverse() if self.ordered else self.order_by('-pk'))[:1])
+        if objects:
+            return objects[0]
+        return None
 
     def in_bulk(self, id_list):
         """
diff --git a/tests/get_earliest_or_latest/models.py b/tests/get_earliest_or_latest/models.py
index cd62cf0104..55096f5a2c 100644
--- a/tests/get_earliest_or_latest/models.py
+++ b/tests/get_earliest_or_latest/models.py
@@ -14,3 +14,18 @@ class Person(models.Model):
     name = models.CharField(max_length=30)
     birthday = models.DateField()
     # Note that this model doesn't have "get_latest_by" set.
+
+
+# Ticket #23555 - model with an intentionally broken QuerySet.__iter__ method.
+
+class IndexErrorQuerySet(models.QuerySet):
+    """
+    Emulates the case when some internal code raises an unexpected
+    IndexError.
+    """
+    def __iter__(self):
+        raise IndexError
+
+
+class IndexErrorArticle(Article):
+    objects = IndexErrorQuerySet.as_manager()
diff --git a/tests/get_earliest_or_latest/tests.py b/tests/get_earliest_or_latest/tests.py
index 742c70bc41..de3307b1a6 100644
--- a/tests/get_earliest_or_latest/tests.py
+++ b/tests/get_earliest_or_latest/tests.py
@@ -4,7 +4,7 @@ from datetime import datetime
 
 from django.test import TestCase
 
-from .models import Article, Person
+from .models import Article, Person, IndexErrorArticle
 
 
 class EarliestOrLatestTests(TestCase):
@@ -122,6 +122,9 @@ class EarliestOrLatestTests(TestCase):
         self.assertRaises(AssertionError, Person.objects.latest)
         self.assertEqual(Person.objects.latest("birthday"), p2)
 
+
+class TestFirstLast(TestCase):
+
     def test_first(self):
         p1 = Person.objects.create(name="Bob", birthday=datetime(1950, 1, 1))
         p2 = Person.objects.create(name="Alice", birthday=datetime(1961, 2, 3))
@@ -152,3 +155,24 @@ class EarliestOrLatestTests(TestCase):
         self.assertIs(
             Person.objects.filter(birthday__lte=datetime(1940, 1, 1)).last(),
             None)
+
+    def test_index_error_not_suppressed(self):
+        """
+        #23555 -- Unexpected IndexError exceptions in QuerySet iteration
+        shouldn't be suppressed.
+        """
+        def check():
+            # We know that we've broken the __iter__ method, so the queryset
+            # should always raise an exception.
+            self.assertRaises(IndexError, lambda: IndexErrorArticle.objects.all()[0])
+            self.assertRaises(IndexError, IndexErrorArticle.objects.all().first)
+            self.assertRaises(IndexError, IndexErrorArticle.objects.all().last)
+
+        check()
+
+        # And it does not matter if there are any records in the DB.
+        IndexErrorArticle.objects.create(
+            headline="Article 1", pub_date=datetime(2005, 7, 26),
+            expire_date=datetime(2005, 9, 1)
+        )
+        check()