From fe54377dae1357a7f102d72614a13f0ef8b2dbdf Mon Sep 17 00:00:00 2001 From: Nick Sandford Date: Sat, 12 Jan 2013 16:37:19 +0800 Subject: [PATCH] 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. --- django/db/models/manager.py | 3 + django/db/models/query.py | 22 +++- docs/ref/models/options.txt | 3 +- docs/ref/models/querysets.txt | 21 ++- docs/releases/1.6.txt | 3 + .../__init__.py | 0 .../models.py | 8 +- .../get_earliest_or_latest/tests.py | 123 ++++++++++++++++++ tests/modeltests/get_latest/tests.py | 58 --------- 9 files changed, 164 insertions(+), 77 deletions(-) rename tests/modeltests/{get_latest => get_earliest_or_latest}/__init__.py (100%) rename tests/modeltests/{get_latest => get_earliest_or_latest}/models.py (83%) create mode 100644 tests/modeltests/get_earliest_or_latest/tests.py delete mode 100644 tests/modeltests/get_latest/tests.py 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 ` 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 ``latest()`` exists purely for convenience and readability. +Note that ``earliest()`` and ``latest()`` exist purely for convenience and +readability. + +earliest +~~~~~~~~ + +.. method:: earliest(field_name=None) + +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 similarity index 100% rename from tests/modeltests/get_latest/__init__.py rename to 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 similarity index 83% rename from tests/modeltests/get_latest/models.py rename to 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)