diff --git a/django/db/models/manager.py b/django/db/models/manager.py index fa13ef625e..3d6d0cdc1b 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,4 +1,3 @@ -from django.db.models.fields import DateField from django.utils.functional import curry from django.db import backend, connection from django.db.models.query import QuerySet @@ -51,9 +50,6 @@ class Manager(QuerySet): def _prepare(self): if self.klass._meta.get_latest_by: self.get_latest = self.__get_latest - for f in self.klass._meta.fields: - if isinstance(f, DateField): - setattr(self, 'get_%s_list' % f.name, curry(self.__get_date_list, f)) def contribute_to_class(self, klass, name): # TODO: Use weakref because of possible memory leak / circular reference. @@ -101,29 +97,6 @@ class Manager(QuerySet): kwargs['limit'] = 1 return self.get_object(*args, **kwargs) - def __get_date_list(self, field, kind, *args, **kwargs): - from django.db.backends.util import typecast_timestamp - assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'." - order = 'ASC' - if kwargs.has_key('order'): - order = kwargs['order'] - del kwargs['order'] - assert order in ('ASC', 'DESC'), "'order' must be either 'ASC' or 'DESC'" - kwargs['order_by'] = () # Clear this because it'll mess things up otherwise. - if field.null: - kwargs.setdefault('where', []).append('%s.%s IS NOT NULL' % \ - (backend.quote_name(self.klass._meta.db_table), backend.quote_name(field.column))) - select, sql, params = self._get_sql_clause(True, *args, **kwargs) - sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \ - (backend.get_date_trunc_sql(kind, '%s.%s' % (backend.quote_name(self.klass._meta.db_table), - backend.quote_name(field.column))), sql, order) - cursor = connection.cursor() - cursor.execute(sql, params) - # We have to manually run typecast_timestamp(str()) on the results, because - # MySQL doesn't automatically cast the result of date functions as datetime - # objects -- MySQL returns the values as strings, instead. - return [typecast_timestamp(str(row[0])) for row in cursor.fetchall()] - class ManagerDescriptor(object): # This class ensures managers aren't accessible via model instances. # For example, Poll.objects works, but poll_obj.objects raises AttributeError. diff --git a/django/db/models/query.py b/django/db/models/query.py index 7d5f6b7c27..818ee367cc 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1,5 +1,5 @@ from django.db import backend, connection -from django.db.models.fields import FieldDoesNotExist +from django.db.models.fields import DateField, FieldDoesNotExist from django.utils.datastructures import SortedDict import copy @@ -180,6 +180,35 @@ class QuerySet(object): _, sql, params = del_query._get_sql_clause(False) cursor.execute("DELETE " + sql, params) + def dates(self, field_name, kind, order='ASC'): + """ + Returns a list of datetime objects representing all available dates + for the given field_name, scoped to 'kind'. + """ + from django.db.backends.util import typecast_timestamp + + assert kind in ("month", "year", "day"), "'kind' must be one of 'year', 'month' or 'day'." + assert order in ('ASC', 'DESC'), "'order' must be either 'ASC' or 'DESC'." + # Let the FieldDoesNotExist exception propogate. + field = self.klass._meta.get_field(field_name, many_to_many=False) + assert isinstance(field, DateField), "%r isn't a DateField." % field_name + + date_query = self._clone() + date_query._order_by = () # Clear this because it'll mess things up otherwise. + if field.null: + date_query._where.append('%s.%s IS NOT NULL' % \ + (backend.quote_name(self.klass._meta.db_table), backend.quote_name(field.column))) + select, sql, params = date_query._get_sql_clause(True) + sql = 'SELECT %s %s GROUP BY 1 ORDER BY 1 %s' % \ + (backend.get_date_trunc_sql(kind, '%s.%s' % (backend.quote_name(self.klass._meta.db_table), + backend.quote_name(field.column))), sql, order) + cursor = connection.cursor() + cursor.execute(sql, params) + # We have to manually run typecast_timestamp(str()) on the results, because + # MySQL doesn't automatically cast the result of date functions as datetime + # objects -- MySQL returns the values as strings, instead. + return [typecast_timestamp(str(row[0])) for row in cursor.fetchall()] + ############################################# # PUBLIC METHODS THAT RETURN A NEW QUERYSET # ############################################# diff --git a/docs/db-api.txt b/docs/db-api.txt index 4b906bb87d..4476ba9bc8 100644 --- a/docs/db-api.txt +++ b/docs/db-api.txt @@ -222,11 +222,11 @@ If you pass an invalid keyword argument, the function will raise ``TypeError``. OR lookups ---------- -By default, keyword argument queries are "AND"ed together. If you have more complex query -requirements (for example, you need to include an ``OR`` statement in your query), you need +By default, keyword argument queries are "AND"ed together. If you have more complex query +requirements (for example, you need to include an ``OR`` statement in your query), you need to use ``Q`` objects. -A ``Q`` object is an instance of ``django.core.meta.Q``, used to encapsulate a collection of +A ``Q`` object is an instance of ``django.core.meta.Q``, used to encapsulate a collection of keyword arguments. These keyword arguments are specified in the same way as keyword arguments to the basic lookup functions like get_object() and get_list(). For example:: @@ -241,14 +241,14 @@ the basic lookup functions like get_object() and get_list(). For example:: ... WHERE question LIKE 'Who%' OR question LIKE 'What%' -You can compose statements of arbitrary complexity by combining ``Q`` objects with the ``&`` and ``|`` operators. Parenthetical grouping can also be used. +You can compose statements of arbitrary complexity by combining ``Q`` objects with the ``&`` and ``|`` operators. Parenthetical grouping can also be used. -One or more ``Q`` objects can then provided as arguments to the lookup functions. If multiple -``Q`` object arguments are provided to a lookup function, they will be "AND"ed together. +One or more ``Q`` objects can then provided as arguments to the lookup functions. If multiple +``Q`` object arguments are provided to a lookup function, they will be "AND"ed together. For example:: polls.get_object( - Q(question__startswith='Who'), + Q(question__startswith='Who'), Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6)) ) @@ -258,8 +258,8 @@ For example:: AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06') If necessary, lookup functions can mix the use of ``Q`` objects and keyword arguments. All arguments -provided to a lookup function (be they keyword argument or ``Q`` object) are "AND"ed together. -However, if a ``Q`` object is provided, it must precede the definition of any keyword arguments. +provided to a lookup function (be they keyword argument or ``Q`` object) are "AND"ed together. +However, if a ``Q`` object is provided, it must precede the definition of any keyword arguments. For example:: polls.get_object( @@ -270,16 +270,16 @@ For example:: # INVALID QUERY polls.get_object( - question__startswith='Who', + question__startswith='Who', Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6))) -... would not be valid. +... would not be valid. A ``Q`` objects can also be provided to the ``complex`` keyword argument. For example:: polls.get_object( - complex=Q(question__startswith='Who') & - (Q(pub_date__exact=date(2005, 5, 2)) | + complex=Q(question__startswith='Who') & + (Q(pub_date__exact=date(2005, 5, 2)) | Q(pub_date__exact=date(2005, 5, 6)) ) ) @@ -549,16 +549,16 @@ deletes the object and has no return value. Example:: >>> c.delete() -Objects can also be deleted in bulk using the same query parameters that are -used for get_object and other query methods. For example:: +Objects can also be deleted in bulk using the same query parameters that are +used for get_object and other query methods. For example:: >>> Polls.objects.delete(pub_date__year=2005) would bulk delete all Polls with a year of 2005. A bulk delete call with no -parameters would theoretically delete all data in the table. To prevent +parameters would theoretically delete all data in the table. To prevent accidental obliteration of a database, a bulk delete query with no parameters -will throw an exception. If you actually want to delete all the data in a -table, you must add a ``DELETE_ALL=True`` argument to your query. +will throw an exception. If you actually want to delete all the data in a +table, you must add a ``DELETE_ALL=True`` argument to your query. For example:: >>> Polls.objects.delete(DELETE_ALL=True) @@ -681,41 +681,43 @@ Extra module functions In addition to every function described in "Basic lookup functions" above, a model module might get any or all of the following methods: -get_FOO_list(kind, \**kwargs) ------------------------------ +dates(field, kind, order='ASC') +------------------------------- -For every ``DateField`` and ``DateTimeField``, the model module will have a -``get_FOO_list()`` function, where ``FOO`` is the name of the field. This -returns a list of ``datetime.datetime`` objects representing all available -dates of the given scope, as defined by the ``kind`` argument. ``kind`` should -be either ``"year"``, ``"month"`` or ``"day"``. Each ``datetime.datetime`` -object in the result list is "truncated" to the given ``type``. +Every manager has a ``dates()`` method, which returns a list of +``datetime.datetime`` objects representing all available dates with the given +filters (if any) and of the given scope, as defined by the ``kind`` argument. + +``field`` should be the name of a ``DateField`` or ``DateTimeField`` of your +model. + +``kind`` should be either ``"year"``, ``"month"`` or ``"day"``. Each +``datetime.datetime`` object in the result list is "truncated" to the given +``type``. * ``"year"`` returns a list of all distinct year values for the field. * ``"month"`` returns a list of all distinct year/month values for the field. * ``"day"`` returns a list of all distinct year/month/day values for the field. -Additional, optional keyword arguments, in the format described in -"Field lookups" above, are also accepted. +``order``, which defaults to ``'ASC'``, should be either ``"ASC"`` or ``"DESC"``. +This specifies how to order the results. Here's an example, using the ``Poll`` model defined above:: >>> from datetime import datetime - >>> p1 = polls.Poll(slug='whatsup', question="What's up?", + >>> p1 = Poll(slug='whatsup', question="What's up?", ... pub_date=datetime(2005, 2, 20), expire_date=datetime(2005, 3, 20)) >>> p1.save() - >>> p2 = polls.Poll(slug='name', question="What's your name?", + >>> p2 = Poll(slug='name', question="What's your name?", ... pub_date=datetime(2005, 3, 20), expire_date=datetime(2005, 4, 20)) >>> p2.save() - >>> polls.get_pub_date_list('year') + >>> Poll.objects.dates('pub_date', 'year') [datetime.datetime(2005, 1, 1)] - >>> polls.get_pub_date_list('month') + >>> Poll.objects.dates('pub_date', 'month') [datetime.datetime(2005, 2, 1), datetime.datetime(2005, 3, 1)] - >>> polls.get_pub_date_list('day') + >>> Poll.objects.dates('pub_date', 'day') [datetime.datetime(2005, 2, 20), datetime.datetime(2005, 3, 20)] - >>> polls.get_pub_date_list('day', question__contains='name') + >>> Poll.objects.dates('pub_date', 'day', order='DESC') + [datetime.datetime(2005, 3, 20), datetime.datetime(2005, 2, 20)] + >>> Poll.objects.filter(question__contains='name').dates('pub_date', 'day') [datetime.datetime(2005, 3, 20)] - -``get_FOO_list()`` also accepts an optional keyword argument ``order``, which -should be either ``"ASC"`` or ``"DESC"``. This specifies how to order the -results. Default is ``"ASC"``. diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py index 07f838e090..85ca1582b8 100644 --- a/tests/modeltests/basic/models.py +++ b/tests/modeltests/basic/models.py @@ -174,26 +174,38 @@ True >>> Article.objects.get(id__exact=8) == Article.objects.get(id__exact=7) False ->>> Article.objects.get_pub_date_list('year') +>>> Article.objects.dates('pub_date', 'year') [datetime.datetime(2005, 1, 1, 0, 0)] ->>> Article.objects.get_pub_date_list('month') +>>> Article.objects.dates('pub_date', 'month') [datetime.datetime(2005, 7, 1, 0, 0)] ->>> Article.objects.get_pub_date_list('day') +>>> Article.objects.dates('pub_date', 'day') [datetime.datetime(2005, 7, 28, 0, 0), datetime.datetime(2005, 7, 29, 0, 0), datetime.datetime(2005, 7, 30, 0, 0), datetime.datetime(2005, 7, 31, 0, 0)] ->>> Article.objects.get_pub_date_list('day', order='ASC') +>>> Article.objects.dates('pub_date', 'day', order='ASC') [datetime.datetime(2005, 7, 28, 0, 0), datetime.datetime(2005, 7, 29, 0, 0), datetime.datetime(2005, 7, 30, 0, 0), datetime.datetime(2005, 7, 31, 0, 0)] ->>> Article.objects.get_pub_date_list('day', order='DESC') +>>> Article.objects.dates('pub_date', 'day', order='DESC') [datetime.datetime(2005, 7, 31, 0, 0), datetime.datetime(2005, 7, 30, 0, 0), datetime.datetime(2005, 7, 29, 0, 0), datetime.datetime(2005, 7, 28, 0, 0)] -# Try some bad arguments to __get_date_list ->>> Article.objects.get_pub_date_list('badarg') +# Try some bad arguments to dates(). + +>>> Article.objects.dates() +Traceback (most recent call last): + ... +TypeError: dates() takes at least 3 arguments (1 given) + +>>> Article.objects.dates('invalid_field', 'year') +Traceback (most recent call last): + ... +FieldDoesNotExist: name=invalid_field + +>>> Article.objects.dates('pub_date', 'bad_kind') Traceback (most recent call last): ... AssertionError: 'kind' must be one of 'year', 'month' or 'day'. ->>> Article.objects.get_pub_date_list(order='ASC') + +>>> Article.objects.dates('pub_date', 'year', order='bad order') Traceback (most recent call last): ... -TypeError: __get_date_list() takes at least 3 non-keyword arguments (2 given) +AssertionError: 'order' must be either 'ASC' or 'DESC'. # You can combine queries with & and | >>> s1 = Article.objects.filter(id__exact=1)