Skip to content

Commit fe54377

Browse files
slurmsakaariai
authored andcommitted
Fixed django#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.
1 parent 37718eb commit fe54377

File tree

9 files changed

+164
-77
lines changed

9 files changed

+164
-77
lines changed

django/db/models/manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ def in_bulk(self, *args, **kwargs):
172172
def iterator(self, *args, **kwargs):
173173
return self.get_query_set().iterator(*args, **kwargs)
174174

175+
def earliest(self, *args, **kwargs):
176+
return self.get_query_set().earliest(*args, **kwargs)
177+
175178
def latest(self, *args, **kwargs):
176179
return self.get_query_set().latest(*args, **kwargs)
177180

django/db/models/query.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
# Pull into this namespace for backwards compatibility.
3030
EmptyResultSet = sql.EmptyResultSet
3131

32+
3233
class QuerySet(object):
3334
"""
3435
Represents a lazy database lookup for a set of objects.
@@ -487,21 +488,28 @@ def get_or_create(self, **kwargs):
487488
# Re-raise the IntegrityError with its original traceback.
488489
six.reraise(*exc_info)
489490

490-
def latest(self, field_name=None):
491+
def _earliest_or_latest(self, field_name=None, direction="-"):
491492
"""
492-
Returns the latest object, according to the model's 'get_latest_by'
493-
option or optional given field_name.
493+
Returns the latest object, according to the model's
494+
'get_latest_by' option or optional given field_name.
494495
"""
495-
latest_by = field_name or self.model._meta.get_latest_by
496-
assert bool(latest_by), "latest() requires either a field_name parameter or 'get_latest_by' in the model"
496+
order_by = field_name or getattr(self.model._meta, 'get_latest_by')
497+
assert bool(order_by), "earliest() and latest() require either a "\
498+
"field_name parameter or 'get_latest_by' in the model"
497499
assert self.query.can_filter(), \
498-
"Cannot change a query once a slice has been taken."
500+
"Cannot change a query once a slice has been taken."
499501
obj = self._clone()
500502
obj.query.set_limits(high=1)
501503
obj.query.clear_ordering()
502-
obj.query.add_ordering('-%s' % latest_by)
504+
obj.query.add_ordering('%s%s' % (direction, order_by))
503505
return obj.get()
504506

507+
def earliest(self, field_name=None):
508+
return self._earliest_or_latest(field_name=field_name, direction="")
509+
510+
def latest(self, field_name=None):
511+
return self._earliest_or_latest(field_name=field_name, direction="-")
512+
505513
def in_bulk(self, id_list):
506514
"""
507515
Returns a dictionary mapping each of the given IDs to the object with

docs/ref/models/options.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ Django quotes column and table names behind the scenes.
8686
The name of an orderable field in the model, typically a :class:`DateField`,
8787
:class:`DateTimeField`, or :class:`IntegerField`. This specifies the default
8888
field to use in your model :class:`Manager`'s
89-
:meth:`~django.db.models.query.QuerySet.latest` method.
89+
:meth:`~django.db.models.query.QuerySet.latest` and
90+
:meth:`~django.db.models.query.QuerySet.earliest` methods.
9091

9192
Example::
9293

docs/ref/models/querysets.txt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1477,14 +1477,23 @@ This example returns the latest ``Entry`` in the table, according to the
14771477

14781478
If your model's :ref:`Meta <meta-options>` specifies
14791479
:attr:`~django.db.models.Options.get_latest_by`, you can leave off the
1480-
``field_name`` argument to ``latest()``. Django will use the field specified
1481-
in :attr:`~django.db.models.Options.get_latest_by` by default.
1480+
``field_name`` argument to ``earliest()`` or ``latest()``. Django will use the
1481+
field specified in :attr:`~django.db.models.Options.get_latest_by` by default.
14821482

1483-
Like :meth:`get()`, ``latest()`` raises
1484-
:exc:`~django.core.exceptions.DoesNotExist` if there is no object with the given
1485-
parameters.
1483+
Like :meth:`get()`, ``earliest()`` and ``latest()`` raise
1484+
:exc:`~django.core.exceptions.DoesNotExist` if there is no object with the
1485+
given parameters.
1486+
1487+
Note that ``earliest()`` and ``latest()`` exist purely for convenience and
1488+
readability.
1489+
1490+
earliest
1491+
~~~~~~~~
1492+
1493+
.. method:: earliest(field_name=None)
14861494

1487-
Note ``latest()`` exists purely for convenience and readability.
1495+
Works otherwise like :meth:`~django.db.models.query.QuerySet.latest` except
1496+
the direction is changed.
14881497

14891498
aggregate
14901499
~~~~~~~~~

docs/releases/1.6.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ Minor features
2828
undefined if the given ``QuerySet`` isn't ordered and there are more than
2929
one ordered values to compare against.
3030

31+
* Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with
32+
:meth:`~django.db.models.query.QuerySet.latest`.
33+
3134
Backwards incompatible changes in 1.6
3235
=====================================
3336

tests/modeltests/get_latest/models.py renamed to tests/modeltests/get_earliest_or_latest/models.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,24 @@
99
"""
1010

1111
from django.db import models
12-
from django.utils.encoding import python_2_unicode_compatible
1312

1413

15-
@python_2_unicode_compatible
1614
class Article(models.Model):
1715
headline = models.CharField(max_length=100)
1816
pub_date = models.DateField()
1917
expire_date = models.DateField()
2018
class Meta:
2119
get_latest_by = 'pub_date'
2220

23-
def __str__(self):
21+
def __unicode__(self):
2422
return self.headline
2523

26-
@python_2_unicode_compatible
24+
2725
class Person(models.Model):
2826
name = models.CharField(max_length=30)
2927
birthday = models.DateField()
3028

3129
# Note that this model doesn't have "get_latest_by" set.
3230

33-
def __str__(self):
31+
def __unicode__(self):
3432
return self.name
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from __future__ import absolute_import
2+
3+
from datetime import datetime
4+
5+
from django.test import TestCase
6+
7+
from .models import Article, Person
8+
9+
10+
class EarliestOrLatestTests(TestCase):
11+
"""Tests for the earliest() and latest() objects methods"""
12+
13+
def tearDown(self):
14+
"""Makes sure Article has a get_latest_by"""
15+
if not Article._meta.get_latest_by:
16+
Article._meta.get_latest_by = 'pub_date'
17+
18+
def test_earliest(self):
19+
# Because no Articles exist yet, earliest() raises ArticleDoesNotExist.
20+
self.assertRaises(Article.DoesNotExist, Article.objects.earliest)
21+
22+
a1 = Article.objects.create(
23+
headline="Article 1", pub_date=datetime(2005, 7, 26),
24+
expire_date=datetime(2005, 9, 1)
25+
)
26+
a2 = Article.objects.create(
27+
headline="Article 2", pub_date=datetime(2005, 7, 27),
28+
expire_date=datetime(2005, 7, 28)
29+
)
30+
a3 = Article.objects.create(
31+
headline="Article 3", pub_date=datetime(2005, 7, 28),
32+
expire_date=datetime(2005, 8, 27)
33+
)
34+
a4 = Article.objects.create(
35+
headline="Article 4", pub_date=datetime(2005, 7, 28),
36+
expire_date=datetime(2005, 7, 30)
37+
)
38+
39+
# Get the earliest Article.
40+
self.assertEqual(Article.objects.earliest(), a1)
41+
# Get the earliest Article that matches certain filters.
42+
self.assertEqual(
43+
Article.objects.filter(pub_date__gt=datetime(2005, 7, 26)).earliest(),
44+
a2
45+
)
46+
47+
# Pass a custom field name to earliest() to change the field that's used
48+
# to determine the earliest object.
49+
self.assertEqual(Article.objects.earliest('expire_date'), a2)
50+
self.assertEqual(Article.objects.filter(
51+
pub_date__gt=datetime(2005, 7, 26)).earliest('expire_date'), a2)
52+
53+
# Ensure that earliest() overrides any other ordering specified on the
54+
# query. Refs #11283.
55+
self.assertEqual(Article.objects.order_by('id').earliest(), a1)
56+
57+
# Ensure that error is raised if the user forgot to add a get_latest_by
58+
# in the Model.Meta
59+
Article.objects.model._meta.get_latest_by = None
60+
self.assertRaisesMessage(
61+
AssertionError,
62+
"earliest() and latest() require either a field_name parameter or "
63+
"'get_latest_by' in the model",
64+
lambda: Article.objects.earliest(),
65+
)
66+
67+
def test_latest(self):
68+
# Because no Articles exist yet, latest() raises ArticleDoesNotExist.
69+
self.assertRaises(Article.DoesNotExist, Article.objects.latest)
70+
71+
a1 = Article.objects.create(
72+
headline="Article 1", pub_date=datetime(2005, 7, 26),
73+
expire_date=datetime(2005, 9, 1)
74+
)
75+
a2 = Article.objects.create(
76+
headline="Article 2", pub_date=datetime(2005, 7, 27),
77+
expire_date=datetime(2005, 7, 28)
78+
)
79+
a3 = Article.objects.create(
80+
headline="Article 3", pub_date=datetime(2005, 7, 27),
81+
expire_date=datetime(2005, 8, 27)
82+
)
83+
a4 = Article.objects.create(
84+
headline="Article 4", pub_date=datetime(2005, 7, 28),
85+
expire_date=datetime(2005, 7, 30)
86+
)
87+
88+
# Get the latest Article.
89+
self.assertEqual(Article.objects.latest(), a4)
90+
# Get the latest Article that matches certain filters.
91+
self.assertEqual(
92+
Article.objects.filter(pub_date__lt=datetime(2005, 7, 27)).latest(),
93+
a1
94+
)
95+
96+
# Pass a custom field name to latest() to change the field that's used
97+
# to determine the latest object.
98+
self.assertEqual(Article.objects.latest('expire_date'), a1)
99+
self.assertEqual(
100+
Article.objects.filter(pub_date__gt=datetime(2005, 7, 26)).latest('expire_date'),
101+
a3,
102+
)
103+
104+
# Ensure that latest() overrides any other ordering specified on the query. Refs #11283.
105+
self.assertEqual(Article.objects.order_by('id').latest(), a4)
106+
107+
# Ensure that error is raised if the user forgot to add a get_latest_by
108+
# in the Model.Meta
109+
Article.objects.model._meta.get_latest_by = None
110+
self.assertRaisesMessage(
111+
AssertionError,
112+
"earliest() and latest() require either a field_name parameter or "
113+
"'get_latest_by' in the model",
114+
lambda: Article.objects.latest(),
115+
)
116+
117+
def test_latest_manual(self):
118+
# You can still use latest() with a model that doesn't have
119+
# "get_latest_by" set -- just pass in the field name manually.
120+
p1 = Person.objects.create(name="Ralph", birthday=datetime(1950, 1, 1))
121+
p2 = Person.objects.create(name="Stephanie", birthday=datetime(1960, 2, 3))
122+
self.assertRaises(AssertionError, Person.objects.latest)
123+
self.assertEqual(Person.objects.latest("birthday"), p2)

tests/modeltests/get_latest/tests.py

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)