summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Sandford <nick@sandford.id.au>2013-01-12 16:37:19 +0800
committerAnssi Kääriäinen <akaariai@gmail.com>2013-01-20 06:39:35 +0200
commitfe54377dae1357a7f102d72614a13f0ef8b2dbdf (patch)
tree73920c4aa4a6d5cfad76a844c35cf25b36583a81
parent37718eb50b60f44462a64f735072e43a9d60b9a6 (diff)
Fixed #17813 -- Added a .earliest() method to QuerySet
Thanks a lot to everybody participating in developing this feature. The patch was developed by multiple people, at least Trac aliases tonnzor, jimmysong, Fandekasp and slurms. Stylistic changes added by committer.
-rw-r--r--django/db/models/manager.py3
-rw-r--r--django/db/models/query.py22
-rw-r--r--docs/ref/models/options.txt3
-rw-r--r--docs/ref/models/querysets.txt21
-rw-r--r--docs/releases/1.6.txt3
-rw-r--r--tests/modeltests/get_earliest_or_latest/__init__.py (renamed from tests/modeltests/get_latest/__init__.py)0
-rw-r--r--tests/modeltests/get_earliest_or_latest/models.py (renamed from tests/modeltests/get_latest/models.py)8
-rw-r--r--tests/modeltests/get_earliest_or_latest/tests.py123
-rw-r--r--tests/modeltests/get_latest/tests.py58
9 files changed, 164 insertions, 77 deletions
diff --git a/django/db/models/manager.py b/django/db/models/manager.py
index da6523c89a..816f6194e3 100644
--- a/django/db/models/manager.py
+++ b/django/db/models/manager.py
@@ -172,6 +172,9 @@ class Manager(object):
def iterator(self, *args, **kwargs):
return self.get_query_set().iterator(*args, **kwargs)
+ def earliest(self, *args, **kwargs):
+ return self.get_query_set().earliest(*args, **kwargs)
+
def latest(self, *args, **kwargs):
return self.get_query_set().latest(*args, **kwargs)
diff --git a/django/db/models/query.py b/django/db/models/query.py
index bdb6d48adc..1c9a68a677 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -29,6 +29,7 @@ REPR_OUTPUT_SIZE = 20
# Pull into this namespace for backwards compatibility.
EmptyResultSet = sql.EmptyResultSet
+
class QuerySet(object):
"""
Represents a lazy database lookup for a set of objects.
@@ -487,21 +488,28 @@ class QuerySet(object):
# Re-raise the IntegrityError with its original traceback.
six.reraise(*exc_info)
- def latest(self, field_name=None):
+ def _earliest_or_latest(self, field_name=None, direction="-"):
"""
- Returns the latest object, according to the model's 'get_latest_by'
- option or optional given field_name.
+ Returns the latest object, according to the model's
+ 'get_latest_by' option or optional given field_name.
"""
- latest_by = field_name or self.model._meta.get_latest_by
- assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model"
+ order_by = field_name or getattr(self.model._meta, 'get_latest_by')
+ assert bool(order_by), "earliest() and latest() require either a "\
+ "field_name parameter or 'get_latest_by' in the model"
assert self.query.can_filter(), \
- "Cannot change a query once a slice has been taken."
+ "Cannot change a query once a slice has been taken."
obj = self._clone()
obj.query.set_limits(high=1)
obj.query.clear_ordering()
- obj.query.add_ordering('-%s' % latest_by)
+ obj.query.add_ordering('%s%s' % (direction, order_by))
return obj.get()
+ def earliest(self, field_name=None):
+ return self._earliest_or_latest(field_name=field_name, direction="")
+
+ def latest(self, field_name=None):
+ return self._earliest_or_latest(field_name=field_name, direction="-")
+
def in_bulk(self, id_list):
"""
Returns a dictionary mapping each of the given IDs to the object with
diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt
index b349197a5b..21265d6313 100644
--- a/docs/ref/models/options.txt
+++ b/docs/ref/models/options.txt
@@ -86,7 +86,8 @@ Django quotes column and table names behind the scenes.
The name of an orderable field in the model, typically a :class:`DateField`,
:class:`DateTimeField`, or :class:`IntegerField`. This specifies the default
field to use in your model :class:`Manager`'s
- :meth:`~django.db.models.query.QuerySet.latest` method.
+ :meth:`~django.db.models.query.QuerySet.latest` and
+ :meth:`~django.db.models.query.QuerySet.earliest` methods.
Example::
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index 71049703c9..1f59ecb4f4 100644
--- a/docs/ref/models/querysets.txt
+++ b/docs/ref/models/querysets.txt
@@ -1477,14 +1477,23 @@ This example returns the latest ``Entry`` in the table, according to the
If your model's :ref:`Meta <meta-options>` specifies
:attr:`~django.db.models.Options.get_latest_by`, you can leave off the
-``field_name`` argument to ``latest()``. Django will use the field specified
-in :attr:`~django.db.models.Options.get_latest_by` by default.
+``field_name`` argument to ``earliest()`` or ``latest()``. Django will use the
+field specified in :attr:`~django.db.models.Options.get_latest_by` by default.
-Like :meth:`get()`, ``latest()`` raises
-:exc:`~django.core.exceptions.DoesNotExist` if there is no object with the given
-parameters.
+Like :meth:`get()`, ``earliest()`` and ``latest()`` raise
+:exc:`~django.core.exceptions.DoesNotExist` if there is no object with the
+given parameters.
+
+Note that ``earliest()`` and ``latest()`` exist purely for convenience and
+readability.
+
+earliest
+~~~~~~~~
+
+.. method:: earliest(field_name=None)
-Note ``latest()`` exists purely for convenience and readability.
+Works otherwise like :meth:`~django.db.models.query.QuerySet.latest` except
+the direction is changed.
aggregate
~~~~~~~~~
diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt
index dcf6f2604a..89d7bb3c05 100644
--- a/docs/releases/1.6.txt
+++ b/docs/releases/1.6.txt
@@ -28,6 +28,9 @@ Minor features
undefined if the given ``QuerySet`` isn't ordered and there are more than
one ordered values to compare against.
+* Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with
+ :meth:`~django.db.models.query.QuerySet.latest`.
+
Backwards incompatible changes in 1.6
=====================================
diff --git a/tests/modeltests/get_latest/__init__.py b/tests/modeltests/get_earliest_or_latest/__init__.py
index e69de29bb2..e69de29bb2 100644
--- a/tests/modeltests/get_latest/__init__.py
+++ b/tests/modeltests/get_earliest_or_latest/__init__.py
diff --git a/tests/modeltests/get_latest/models.py b/tests/modeltests/get_earliest_or_latest/models.py
index fe594dd802..2453eaaccd 100644
--- a/tests/modeltests/get_latest/models.py
+++ b/tests/modeltests/get_earliest_or_latest/models.py
@@ -9,10 +9,8 @@ farthest into the future."
"""
from django.db import models
-from django.utils.encoding import python_2_unicode_compatible
-@python_2_unicode_compatible
class Article(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateField()
@@ -20,15 +18,15 @@ class Article(models.Model):
class Meta:
get_latest_by = 'pub_date'
- def __str__(self):
+ def __unicode__(self):
return self.headline
-@python_2_unicode_compatible
+
class Person(models.Model):
name = models.CharField(max_length=30)
birthday = models.DateField()
# Note that this model doesn't have "get_latest_by" set.
- def __str__(self):
+ def __unicode__(self):
return self.name
diff --git a/tests/modeltests/get_earliest_or_latest/tests.py b/tests/modeltests/get_earliest_or_latest/tests.py
new file mode 100644
index 0000000000..6317a0974c
--- /dev/null
+++ b/tests/modeltests/get_earliest_or_latest/tests.py
@@ -0,0 +1,123 @@
+from __future__ import absolute_import
+
+from datetime import datetime
+
+from django.test import TestCase
+
+from .models import Article, Person
+
+
+class EarliestOrLatestTests(TestCase):
+ """Tests for the earliest() and latest() objects methods"""
+
+ def tearDown(self):
+ """Makes sure Article has a get_latest_by"""
+ if not Article._meta.get_latest_by:
+ Article._meta.get_latest_by = 'pub_date'
+
+ def test_earliest(self):
+ # Because no Articles exist yet, earliest() raises ArticleDoesNotExist.
+ self.assertRaises(Article.DoesNotExist, Article.objects.earliest)
+
+ a1 = Article.objects.create(
+ headline="Article 1", pub_date=datetime(2005, 7, 26),
+ expire_date=datetime(2005, 9, 1)
+ )
+ a2 = Article.objects.create(
+ headline="Article 2", pub_date=datetime(2005, 7, 27),
+ expire_date=datetime(2005, 7, 28)
+ )
+ a3 = Article.objects.create(
+ headline="Article 3", pub_date=datetime(2005, 7, 28),
+ expire_date=datetime(2005, 8, 27)
+ )
+ a4 = Article.objects.create(
+ headline="Article 4", pub_date=datetime(2005, 7, 28),
+ expire_date=datetime(2005, 7, 30)
+ )
+
+ # Get the earliest Article.
+ self.assertEqual(Article.objects.earliest(), a1)
+ # Get the earliest Article that matches certain filters.
+ self.assertEqual(
+ Article.objects.filter(pub_date__gt=datetime(2005, 7, 26)).earliest(),
+ a2
+ )
+
+ # Pass a custom field name to earliest() to change the field that's used
+ # to determine the earliest object.
+ self.assertEqual(Article.objects.earliest('expire_date'), a2)
+ self.assertEqual(Article.objects.filter(
+ pub_date__gt=datetime(2005, 7, 26)).earliest('expire_date'), a2)
+
+ # Ensure that earliest() overrides any other ordering specified on the
+ # query. Refs #11283.
+ self.assertEqual(Article.objects.order_by('id').earliest(), a1)
+
+ # Ensure that error is raised if the user forgot to add a get_latest_by
+ # in the Model.Meta
+ Article.objects.model._meta.get_latest_by = None
+ self.assertRaisesMessage(
+ AssertionError,
+ "earliest() and latest() require either a field_name parameter or "
+ "'get_latest_by' in the model",
+ lambda: Article.objects.earliest(),
+ )
+
+ def test_latest(self):
+ # Because no Articles exist yet, latest() raises ArticleDoesNotExist.
+ self.assertRaises(Article.DoesNotExist, Article.objects.latest)
+
+ a1 = Article.objects.create(
+ headline="Article 1", pub_date=datetime(2005, 7, 26),
+ expire_date=datetime(2005, 9, 1)
+ )
+ a2 = Article.objects.create(
+ headline="Article 2", pub_date=datetime(2005, 7, 27),
+ expire_date=datetime(2005, 7, 28)
+ )
+ a3 = Article.objects.create(
+ headline="Article 3", pub_date=datetime(2005, 7, 27),
+ expire_date=datetime(2005, 8, 27)
+ )
+ a4 = Article.objects.create(
+ headline="Article 4", pub_date=datetime(2005, 7, 28),
+ expire_date=datetime(2005, 7, 30)
+ )
+
+ # Get the latest Article.
+ self.assertEqual(Article.objects.latest(), a4)
+ # Get the latest Article that matches certain filters.
+ self.assertEqual(
+ Article.objects.filter(pub_date__lt=datetime(2005, 7, 27)).latest(),
+ a1
+ )
+
+ # Pass a custom field name to latest() to change the field that's used
+ # to determine the latest object.
+ self.assertEqual(Article.objects.latest('expire_date'), a1)
+ self.assertEqual(
+ Article.objects.filter(pub_date__gt=datetime(2005, 7, 26)).latest('expire_date'),
+ a3,
+ )
+
+ # Ensure that latest() overrides any other ordering specified on the query. Refs #11283.
+ self.assertEqual(Article.objects.order_by('id').latest(), a4)
+
+ # Ensure that error is raised if the user forgot to add a get_latest_by
+ # in the Model.Meta
+ Article.objects.model._meta.get_latest_by = None
+ self.assertRaisesMessage(
+ AssertionError,
+ "earliest() and latest() require either a field_name parameter or "
+ "'get_latest_by' in the model",
+ lambda: Article.objects.latest(),
+ )
+
+ def test_latest_manual(self):
+ # You can still use latest() with a model that doesn't have
+ # "get_latest_by" set -- just pass in the field name manually.
+ p1 = Person.objects.create(name="Ralph", birthday=datetime(1950, 1, 1))
+ p2 = Person.objects.create(name="Stephanie", birthday=datetime(1960, 2, 3))
+ self.assertRaises(AssertionError, Person.objects.latest)
+ self.assertEqual(Person.objects.latest("birthday"), p2)
diff --git a/tests/modeltests/get_latest/tests.py b/tests/modeltests/get_latest/tests.py
deleted file mode 100644
index 948af6045a..0000000000
--- a/tests/modeltests/get_latest/tests.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from __future__ import absolute_import
-
-from datetime import datetime
-
-from django.test import TestCase
-
-from .models import Article, Person
-
-
-class LatestTests(TestCase):
- def test_latest(self):
- # Because no Articles exist yet, latest() raises ArticleDoesNotExist.
- self.assertRaises(Article.DoesNotExist, Article.objects.latest)
-
- a1 = Article.objects.create(
- headline="Article 1", pub_date=datetime(2005, 7, 26),
- expire_date=datetime(2005, 9, 1)
- )
- a2 = Article.objects.create(
- headline="Article 2", pub_date=datetime(2005, 7, 27),
- expire_date=datetime(2005, 7, 28)
- )
- a3 = Article.objects.create(
- headline="Article 3", pub_date=datetime(2005, 7, 27),
- expire_date=datetime(2005, 8, 27)
- )
- a4 = Article.objects.create(
- headline="Article 4", pub_date=datetime(2005, 7, 28),
- expire_date=datetime(2005, 7, 30)
- )
-
- # Get the latest Article.
- self.assertEqual(Article.objects.latest(), a4)
- # Get the latest Article that matches certain filters.
- self.assertEqual(
- Article.objects.filter(pub_date__lt=datetime(2005, 7, 27)).latest(),
- a1
- )
-
- # Pass a custom field name to latest() to change the field that's used
- # to determine the latest object.
- self.assertEqual(Article.objects.latest('expire_date'), a1)
- self.assertEqual(
- Article.objects.filter(pub_date__gt=datetime(2005, 7, 26)).latest('expire_date'),
- a3,
- )
-
- # Ensure that latest() overrides any other ordering specified on the query. Refs #11283.
- self.assertEqual(Article.objects.order_by('id').latest(), a4)
-
- def test_latest_manual(self):
- # You can still use latest() with a model that doesn't have
- # "get_latest_by" set -- just pass in the field name manually.
- p1 = Person.objects.create(name="Ralph", birthday=datetime(1950, 1, 1))
- p2 = Person.objects.create(name="Stephanie", birthday=datetime(1960, 2, 3))
- self.assertRaises(AssertionError, Person.objects.latest)
-
- self.assertEqual(Person.objects.latest("birthday"), p2)