diff --git a/AUTHORS b/AUTHORS index 5f36d83770..c965ddee4e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -131,6 +131,7 @@ answer newbie questions, and generally made Django that much better: Andrew Durdin dusk@woofle.net Andy Dustman + J. Clifford Dyer Clint Ecker Nick Efford eibaan@gmail.com diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 6e9ef1161f..16b254ed20 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -114,20 +114,20 @@ class AdminSite(object): name = name or action.__name__ self._actions[name] = action self._global_actions[name] = action - + def disable_action(self, name): """ Disable a globally-registered action. Raises KeyError for invalid names. """ del self._actions[name] - + def get_action(self, name): """ Explicitally get a registered global action wheather it's enabled or not. Raises KeyError for invalid names. """ return self._global_actions[name] - + def actions(self): """ Get all the enabled actions as an iterable of (name, func). @@ -159,9 +159,9 @@ class AdminSite(object): if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.") - def admin_view(self, view): + def admin_view(self, view, cacheable=False): """ - Decorator to create an "admin view attached to this ``AdminSite``. This + Decorator to create an admin view attached to this ``AdminSite``. This wraps the view and provides permission checking by calling ``self.has_permission``. @@ -177,19 +177,25 @@ class AdminSite(object): url(r'^my_view/$', self.admin_view(some_view)) ) return urls + + By default, admin_views are marked non-cacheable using the + ``never_cache`` decorator. If the view can be safely cached, set + cacheable=True. """ def inner(request, *args, **kwargs): if not self.has_permission(request): return self.login(request) return view(request, *args, **kwargs) + if not cacheable: + inner = never_cache(inner) return update_wrapper(inner, view) def get_urls(self): from django.conf.urls.defaults import patterns, url, include - def wrap(view): + def wrap(view, cacheable=False): def wrapper(*args, **kwargs): - return self.admin_view(view)(*args, **kwargs) + return self.admin_view(view, cacheable)(*args, **kwargs) return update_wrapper(wrapper, view) # Admin-site-wide views. @@ -201,13 +207,13 @@ class AdminSite(object): wrap(self.logout), name='%sadmin_logout'), url(r'^password_change/$', - wrap(self.password_change), + wrap(self.password_change, cacheable=True), name='%sadmin_password_change' % self.name), url(r'^password_change/done/$', - wrap(self.password_change_done), + wrap(self.password_change_done, cacheable=True), name='%sadmin_password_change_done' % self.name), url(r'^jsi18n/$', - wrap(self.i18n_javascript), + wrap(self.i18n_javascript, cacheable=True), name='%sadmin_jsi18n' % self.name), url(r'^r/(?P\d+)/(?P.+)/$', 'django.views.defaults.shortcut'), diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index 2bfda990ff..eac66f4a83 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -19,6 +19,9 @@ class GeoManager(Manager): def centroid(self, *args, **kwargs): return self.get_query_set().centroid(*args, **kwargs) + def collect(self, *args, **kwargs): + return self.get_query_set().collect(*args, **kwargs) + def difference(self, *args, **kwargs): return self.get_query_set().difference(*args, **kwargs) diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 3d31f73c29..d055260a58 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -62,6 +62,14 @@ class GeoQuerySet(QuerySet): """ return self._geom_attribute('centroid', **kwargs) + def collect(self, **kwargs): + """ + Performs an aggregate collect operation on the given geometry field. + This is analagous to a union operation, but much faster because + boundaries are not dissolved. + """ + return self._spatial_aggregate(aggregates.Collect, **kwargs) + def difference(self, geom, **kwargs): """ Returns the spatial difference of the geographic field in a `difference` diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 502a3c0be9..81bce610be 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -1,7 +1,7 @@ import os, unittest from django.contrib.gis.geos import * from django.contrib.gis.db.backend import SpatialBackend -from django.contrib.gis.db.models import Count, Extent, F, Union +from django.contrib.gis.db.models import Collect, Count, Extent, F, Union from django.contrib.gis.tests.utils import no_mysql, no_oracle, no_spatialite from django.conf import settings from models import City, Location, DirectoryEntry, Parcel, Book, Author @@ -237,7 +237,7 @@ class RelatedGeoModelTest(unittest.TestCase): # as Dallas. dallas = City.objects.get(name='Dallas') ftworth = City.objects.create(name='Fort Worth', state='TX', location=dallas.location) - + # Count annotation should be 2 for the Dallas location now. loc = Location.objects.annotate(num_cities=Count('city')).get(id=dallas.location.id) self.assertEqual(2, loc.num_cities) @@ -250,7 +250,7 @@ class RelatedGeoModelTest(unittest.TestCase): Book.objects.create(title='Blank Spots on the Map', author=tp) wp = Author.objects.create(name='William Patry') Book.objects.create(title='Patry on Copyright', author=wp) - + # Should only be one author (Trevor Paglen) returned by this query, and # the annotation should have 3 for the number of books. qs = Author.objects.annotate(num_books=Count('books')).filter(num_books__gt=1) @@ -264,6 +264,27 @@ class RelatedGeoModelTest(unittest.TestCase): # Should be `None`, and not a 'dummy' model. self.assertEqual(None, b.author) + @no_mysql + @no_oracle + @no_spatialite + def test14_collect(self): + "Testing the `collect` GeoQuerySet method and `Collect` aggregate." + # Reference query: + # SELECT AsText(ST_Collect("relatedapp_location"."point")) FROM "relatedapp_city" LEFT OUTER JOIN + # "relatedapp_location" ON ("relatedapp_city"."location_id" = "relatedapp_location"."id") + # WHERE "relatedapp_city"."state" = 'TX'; + ref_geom = fromstr('MULTIPOINT(-97.516111 33.058333,-96.801611 32.782057,-95.363151 29.763374,-96.801611 32.782057)') + + c1 = City.objects.filter(state='TX').collect(field_name='location__point') + c2 = City.objects.filter(state='TX').aggregate(Collect('location__point'))['location__point__collect'] + + for coll in (c1, c2): + # Even though Dallas and Ft. Worth share same point, Collect doesn't + # consolidate -- that's why 4 points in MultiPoint. + self.assertEqual(4, len(coll)) + self.assertEqual(ref_geom, coll) + + # TODO: Related tests for KML, GML, and distance lookups. def suite(): diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index a0fa06283e..f2326aa283 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -217,12 +217,13 @@ WHEN (new.%(col_name)s IS NULL) # continue to loop break for f in model._meta.many_to_many: - table_name = self.quote_name(f.m2m_db_table()) - sequence_name = get_sequence_name(f.m2m_db_table()) - column_name = self.quote_name('id') - output.append(query % {'sequence': sequence_name, - 'table': table_name, - 'column': column_name}) + if not f.rel.through: + table_name = self.quote_name(f.m2m_db_table()) + sequence_name = get_sequence_name(f.m2m_db_table()) + column_name = self.quote_name('id') + output.append(query % {'sequence': sequence_name, + 'table': table_name, + 'column': column_name}) return output def start_transaction_sql(self): diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py index bb0d64515a..e2bc992e49 100644 --- a/django/db/backends/postgresql/operations.py +++ b/django/db/backends/postgresql/operations.py @@ -121,14 +121,15 @@ class DatabaseOperations(BaseDatabaseOperations): style.SQL_TABLE(qn(model._meta.db_table)))) break # Only one AutoField is allowed per model, so don't bother continuing. for f in model._meta.many_to_many: - output.append("%s setval('%s', coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \ - (style.SQL_KEYWORD('SELECT'), - style.SQL_FIELD(qn('%s_id_seq' % f.m2m_db_table())), - style.SQL_FIELD(qn('id')), - style.SQL_FIELD(qn('id')), - style.SQL_KEYWORD('IS NOT'), - style.SQL_KEYWORD('FROM'), - style.SQL_TABLE(qn(f.m2m_db_table())))) + if not f.rel.through: + output.append("%s setval('%s', coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \ + (style.SQL_KEYWORD('SELECT'), + style.SQL_FIELD(qn('%s_id_seq' % f.m2m_db_table())), + style.SQL_FIELD(qn('id')), + style.SQL_FIELD(qn('id')), + style.SQL_KEYWORD('IS NOT'), + style.SQL_KEYWORD('FROM'), + style.SQL_TABLE(qn(f.m2m_db_table())))) return output def savepoint_create_sql(self, sid): diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index bbad89acf9..307e2c05d7 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -464,7 +464,7 @@ should raise either a ``ValueError`` if the ``value`` is of the wrong sort (a list when you were expecting an object, for example) or a ``TypeError`` if your field does not support that type of lookup. For many fields, you can get by with handling the lookup types that need special handling for your field -and pass the rest of the :meth:`get_db_prep_lookup` method of the parent class. +and pass the rest to the :meth:`get_db_prep_lookup` method of the parent class. If you needed to implement ``get_db_prep_save()``, you will usually need to implement ``get_db_prep_lookup()``. If you don't, ``get_db_prep_value`` will be diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index e0750ce327..246e7445d0 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -23,6 +23,10 @@ administrators immediate notification of any errors. The :setting:`ADMINS` will get a description of the error, a complete Python traceback, and details about the HTTP request that caused the error. +By default, Django will send email from root@localhost. However, some mail +providers reject all email from this address. To use a different sender +address, modify the :setting:`SERVER_EMAIL` setting. + To disable this behavior, just remove all entries from the :setting:`ADMINS` setting. @@ -33,12 +37,12 @@ Django can also be configured to email errors about broken links (404 "page not found" errors). Django sends emails about 404 errors when: * :setting:`DEBUG` is ``False`` - + * :setting:`SEND_BROKEN_LINK_EMAILS` is ``True`` - + * Your :setting:`MIDDLEWARE_CLASSES` setting includes ``CommonMiddleware`` (which it does by default). - + If those conditions are met, Django will e-mail the users listed in the :setting:`MANAGERS` setting whenever your code raises a 404 and the request has a referer. (It doesn't bother to e-mail for 404s that don't have a referer -- diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index f4ef5f76fe..687407a284 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -365,7 +365,7 @@ That takes care of setting ``handler404`` in the current module. As you can see in ``django/conf/urls/defaults.py``, ``handler404`` is set to :func:`django.views.defaults.page_not_found` by default. -Three more things to note about 404 views: +Four more things to note about 404 views: * If :setting:`DEBUG` is set to ``True`` (in your settings module) then your 404 view will never be used (and thus the ``404.html`` template will never diff --git a/docs/ref/contrib/admin/_images/article_actions.png b/docs/ref/contrib/admin/_images/article_actions.png index 254a8ad557..df4ab8f1ec 100644 Binary files a/docs/ref/contrib/admin/_images/article_actions.png and b/docs/ref/contrib/admin/_images/article_actions.png differ diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 4b1dc55b21..d74cb0c55a 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -762,12 +762,19 @@ documented in :ref:`topics-http-urls`:: anything, so you'll usually want to prepend your custom URLs to the built-in ones. -Note, however, that the ``self.my_view`` function registered above will *not* -have any permission check done; it'll be accessible to the general public. Since -this is usually not what you want, Django provides a convience wrapper to check -permissions. This wrapper is :meth:`AdminSite.admin_view` (i.e. -``self.admin_site.admin_view`` inside a ``ModelAdmin`` instance); use it like -so:: +However, the ``self.my_view`` function registered above suffers from two +problems: + + * It will *not* perform and permission checks, so it will be accessible to + the general public. + * It will *not* provide any header details to prevent caching. This means if + the page retrieves data from the database, and caching middleware is + active, the page could show outdated information. + +Since this is usually not what you want, Django provides a convenience wrapper +to check permissions and mark the view as non-cacheable. This wrapper is +:meth:`AdminSite.admin_view` (i.e. ``self.admin_site.admin_view`` inside a +``ModelAdmin`` instance); use it like so:: class MyModelAdmin(admin.ModelAdmin): def get_urls(self): @@ -781,7 +788,14 @@ Notice the wrapped view in the fifth line above:: (r'^my_view/$', self.admin_site.admin_view(self.my_view)) -This wrapping will protect ``self.my_view`` from unauthorized access. +This wrapping will protect ``self.my_view`` from unauthorized access and will +apply the ``django.views.decorators.cache.never_cache`` decorator to make sure +it is not cached if the cache middleware is active. + +If the page is cacheable, but you still want the permission check to be performed, +you can pass a ``cacheable=True`` argument to :meth:`AdminSite.admin_view`:: + + (r'^my_view/$', self.admin_site.admin_view(self.my_view, cacheable=True)) .. method:: ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs) @@ -849,7 +863,7 @@ provided some extra mapping data that would not otherwise be available:: 'osm_data': self.get_osm_info(), } return super(MyModelAdmin, self).change_view(request, object_id, - extra_context=my_context)) + extra_context=my_context) ``ModelAdmin`` media definitions -------------------------------- diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 94900b3892..8a926afc97 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -177,9 +177,9 @@ The ``ContentTypeManager`` .. method:: models.ContentTypeManager.clear_cache() Clears an internal cache used by - :class:`~django.contrib.contenttypes.models.ContentType>` to keep track + :class:`~django.contrib.contenttypes.models.ContentType` to keep track of which models for which it has created - :class:`django.contrib.contenttypes.models.ContentType>` instances. You + :class:`django.contrib.contenttypes.models.ContentType` instances. You probably won't ever need to call this method yourself; Django will call it automatically when it's needed. diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index e532971179..4bb6a7c444 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -275,7 +275,7 @@ For each field, we describe the default widget used if you don't specify * Default widget: ``CheckboxInput`` * Empty value: ``False`` * Normalizes to: A Python ``True`` or ``False`` value. - * Validates that the check box is checked (i.e. the value is ``True``) if + * Validates that the value is ``True`` (e.g. the check box is checked) if the field has ``required=True``. * Error message keys: ``required`` @@ -287,9 +287,10 @@ For each field, we describe the default widget used if you don't specify .. note:: Since all ``Field`` subclasses have ``required=True`` by default, the - validation condition here is important. If you want to include a checkbox - in your form that can be either checked or unchecked, you must remember to - pass in ``required=False`` when creating the ``BooleanField``. + validation condition here is important. If you want to include a boolean + in your form that can be either ``True`` or ``False`` (e.g. a checked or + unchecked checkbox), you must remember to pass in ``required=False`` when + creating the ``BooleanField``. ``CharField`` ~~~~~~~~~~~~~ @@ -328,7 +329,7 @@ Takes one extra required argument: An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this field. - + ``TypedChoiceField`` ~~~~~~~~~~~~~~~~~~~~ @@ -437,7 +438,7 @@ If no ``input_formats`` argument is provided, the default input formats are:: ``min_value``, ``max_digits``, ``max_decimal_places``, ``max_whole_digits`` -Takes four optional arguments: +Takes four optional arguments: .. attribute:: DecimalField.max_value .. attribute:: DecimalField.min_value @@ -449,7 +450,7 @@ Takes four optional arguments: The maximum number of digits (those before the decimal point plus those after the decimal point, with leading zeros stripped) permitted in the value. - + .. attribute:: DecimalField.decimal_places The maximum number of decimal places permitted. @@ -522,18 +523,18 @@ extra arguments; only ``path`` is required: A regular expression pattern; only files with names matching this expression will be allowed as choices. -``FloatField`` -~~~~~~~~~~~~~~ +``FloatField`` +~~~~~~~~~~~~~~ - * Default widget: ``TextInput`` - * Empty value: ``None`` - * Normalizes to: A Python float. - * Validates that the given value is an float. Leading and trailing - whitespace is allowed, as in Python's ``float()`` function. - * Error message keys: ``required``, ``invalid``, ``max_value``, - ``min_value`` - -Takes two optional arguments for validation, ``max_value`` and ``min_value``. + * Default widget: ``TextInput`` + * Empty value: ``None`` + * Normalizes to: A Python float. + * Validates that the given value is an float. Leading and trailing + whitespace is allowed, as in Python's ``float()`` function. + * Error message keys: ``required``, ``invalid``, ``max_value``, + ``min_value`` + +Takes two optional arguments for validation, ``max_value`` and ``min_value``. These control the range of values permitted in the field. ``ImageField`` @@ -779,10 +780,10 @@ example:: (which is ``"---------"`` by default) with the ``empty_label`` attribute, or you can disable the empty label entirely by setting ``empty_label`` to ``None``:: - + # A custom empty label field1 = forms.ModelChoiceField(queryset=..., empty_label="(Nothing)") - + # No empty label field2 = forms.ModelChoiceField(queryset=..., empty_label=None) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index b97e3620e2..f58e5ed53a 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -668,7 +668,7 @@ of the arguments is required, but you should use at least one of them. The resulting SQL of the above example would be:: - SELECT blog_blog.*, (SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id) + SELECT blog_blog.*, (SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id) AS entry_count FROM blog_blog; Note that the parenthesis required by most database engines around diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 8e90b54ced..e6146aeaba 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -86,9 +86,9 @@ displayed. Formset validation ------------------ -Validation with a formset is about identical to a regular ``Form``. There is +Validation with a formset is almost identical to a regular ``Form``. There is an ``is_valid`` method on the formset to provide a convenient way to validate -each form in the formset:: +all forms in the formset:: >>> ArticleFormSet = formset_factory(ArticleForm) >>> formset = ArticleFormSet({}) @@ -97,22 +97,25 @@ each form in the formset:: We passed in no data to the formset which is resulting in a valid form. The formset is smart enough to ignore extra forms that were not changed. If we -attempt to provide an article, but fail to do so:: +provide an invalid article:: >>> data = { - ... 'form-TOTAL_FORMS': u'1', - ... 'form-INITIAL_FORMS': u'1', + ... 'form-TOTAL_FORMS': u'2', + ... 'form-INITIAL_FORMS': u'0', ... 'form-0-title': u'Test', - ... 'form-0-pub_date': u'', + ... 'form-0-pub_date': u'16 June 1904', + ... 'form-1-title': u'Test', + ... 'form-1-pub_date': u'', # <-- this date is missing but required ... } >>> formset = ArticleFormSet(data) >>> formset.is_valid() False >>> formset.errors - [{'pub_date': [u'This field is required.']}] + [{}, {'pub_date': [u'This field is required.']}] -As we can see the formset properly performed validation and gave us the -expected errors. +As we can see, ``formset.errors`` is a list whose entries correspond to the +forms in the formset. Validation was performed for each of the two forms, and +the expected error message appears for the second item. .. _understanding-the-managementform: @@ -155,20 +158,40 @@ Custom formset validation ~~~~~~~~~~~~~~~~~~~~~~~~~ A formset has a ``clean`` method similar to the one on a ``Form`` class. This -is where you define your own validation that deals at the formset level:: +is where you define your own validation that works at the formset level:: >>> from django.forms.formsets import BaseFormSet >>> class BaseArticleFormSet(BaseFormSet): ... def clean(self): - ... raise forms.ValidationError, u'An error occured.' + ... """Checks that no two articles have the same title.""" + ... if any(self.errors): + ... # Don't bother validating the formset unless each form is valid on its own + ... return + ... titles = [] + ... for i in range(0, self.total_form_count()): + ... form = self.forms[i] + ... title = form.cleaned_data['title'] + ... if title in titles: + ... raise forms.ValidationError, "Articles in a set must have distinct titles." + ... titles.append(title) >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet) - >>> formset = ArticleFormSet({}) + >>> data = { + ... 'form-TOTAL_FORMS': u'2', + ... 'form-INITIAL_FORMS': u'0', + ... 'form-0-title': u'Test', + ... 'form-0-pub_date': u'16 June 1904', + ... 'form-1-title': u'Test', + ... 'form-1-pub_date': u'23 June 1912', + ... } + >>> formset = ArticleFormSet(data) >>> formset.is_valid() False + >>> formset.errors + [{}, {}] >>> formset.non_form_errors() - [u'An error occured.'] + [u'Articles in a set must have distinct titles.'] The formset ``clean`` method is called after all the ``Form.clean`` methods have been called. The errors will be found using the ``non_form_errors()`` diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 4248d4f02e..17978d4328 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -40,14 +40,14 @@ algorithm the system follows to determine which Python code to execute: this is the value of the ``ROOT_URLCONF`` setting, but if the incoming ``HttpRequest`` object has an attribute called ``urlconf``, its value will be used in place of the ``ROOT_URLCONF`` setting. - + 2. Django loads that Python module and looks for the variable ``urlpatterns``. This should be a Python list, in the format returned by the function ``django.conf.urls.defaults.patterns()``. - + 3. Django runs through each URL pattern, in order, and stops at the first one that matches the requested URL. - + 4. Once one of the regexes matches, Django imports and calls the given view, which is a simple Python function. The view gets passed an :class:`~django.http.HttpRequest` as its first argument and any values @@ -263,8 +263,15 @@ value should suffice. include ------- -A function that takes a full Python import path to another URLconf that should -be "included" in this place. See `Including other URLconfs`_ below. +A function that takes a full Python import path to another URLconf module that +should be "included" in this place. + +.. versionadded:: 1.1 + +:meth:``include`` also accepts as an argument an iterable that returns URL +patterns. + +See `Including other URLconfs`_ below. Notes on capturing text in URLs =============================== @@ -391,6 +398,25 @@ Django encounters ``include()``, it chops off whatever part of the URL matched up to that point and sends the remaining string to the included URLconf for further processing. +.. versionadded:: 1.1 + +Another posibility is to include additional URL patterns not by specifying the +URLconf Python module defining them as the `include`_ argument but by using +directly the pattern list as returned by `patterns`_ instead. For example:: + + from django.conf.urls.defaults import * + + extra_patterns = patterns('', + url(r'reports/(?P\d+)/$', 'credit.views.report', name='credit-reports'), + url(r'charge/$', 'credit.views.charge', name='credit-charge'), + ) + + urlpatterns = patterns('', + url(r'^$', 'apps.main.views.homepage', name='site-homepage'), + (r'^help/', include('apps.help.urls')), + (r'^credit/', include(extra_patterns)), + ) + .. _`Django Web site`: http://www.djangoproject.com/ Captured parameters diff --git a/docs/topics/i18n.txt b/docs/topics/i18n.txt index 7bf51c11c5..c5f4ab6481 100644 --- a/docs/topics/i18n.txt +++ b/docs/topics/i18n.txt @@ -959,11 +959,11 @@ Using the JavaScript translation catalog To use the catalog, just pull in the dynamically generated script like this:: - + -This is how the admin fetches the translation catalog from the server. When the -catalog is loaded, your JavaScript code can use the standard ``gettext`` -interface to access it:: +This uses reverse URL lookup to find the URL of the JavaScript catalog view. +When the catalog is loaded, your JavaScript code can use the standard +``gettext`` interface to access it:: document.write(gettext('this is to be translated')); diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 99168fdeee..65e9d9c28e 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -10,6 +10,7 @@ from django.contrib.admin.models import LogEntry, DELETION from django.contrib.admin.sites import LOGIN_FORM_KEY from django.contrib.admin.util import quote from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME +from django.utils.cache import get_max_age from django.utils.html import escape # local test models @@ -1527,3 +1528,75 @@ class AdminInlineTests(TestCase): self.failUnlessEqual(Category.objects.get(id=2).order, 13) self.failUnlessEqual(Category.objects.get(id=3).order, 1) self.failUnlessEqual(Category.objects.get(id=4).order, 0) + + +class NeverCacheTests(TestCase): + fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def testAdminIndex(self): + "Check the never-cache status of the main index" + response = self.client.get('/test_admin/admin/') + self.failUnlessEqual(get_max_age(response), 0) + + def testAppIndex(self): + "Check the never-cache status of an application index" + response = self.client.get('/test_admin/admin/admin_views/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelIndex(self): + "Check the never-cache status of a model index" + response = self.client.get('/test_admin/admin/admin_views/fabric/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelAdd(self): + "Check the never-cache status of a model add page" + response = self.client.get('/test_admin/admin/admin_views/fabric/add/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelView(self): + "Check the never-cache status of a model edit page" + response = self.client.get('/test_admin/admin/admin_views/section/1/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelHistory(self): + "Check the never-cache status of a model history page" + response = self.client.get('/test_admin/admin/admin_views/section/1/history/') + self.failUnlessEqual(get_max_age(response), 0) + + def testModelDelete(self): + "Check the never-cache status of a model delete page" + response = self.client.get('/test_admin/admin/admin_views/section/1/delete/') + self.failUnlessEqual(get_max_age(response), 0) + + def testLogin(self): + "Check the never-cache status of login views" + self.client.logout() + response = self.client.get('/test_admin/admin/') + self.failUnlessEqual(get_max_age(response), 0) + + def testLogout(self): + "Check the never-cache status of logout view" + response = self.client.get('/test_admin/admin/logout/') + self.failUnlessEqual(get_max_age(response), 0) + + def testPasswordChange(self): + "Check the never-cache status of the password change view" + self.client.logout() + response = self.client.get('/test_admin/password_change/') + self.failUnlessEqual(get_max_age(response), None) + + def testPasswordChangeDone(self): + "Check the never-cache status of the password change done view" + response = self.client.get('/test_admin/admin/password_change/done/') + self.failUnlessEqual(get_max_age(response), None) + + def testJsi18n(self): + "Check the never-cache status of the Javascript i18n view" + response = self.client.get('/test_admin/jsi18n/') + self.failUnlessEqual(get_max_age(response), None) diff --git a/tests/regressiontests/m2m_through_regress/fixtures/m2m_through.json b/tests/regressiontests/m2m_through_regress/fixtures/m2m_through.json new file mode 100644 index 0000000000..6f24886f02 --- /dev/null +++ b/tests/regressiontests/m2m_through_regress/fixtures/m2m_through.json @@ -0,0 +1,34 @@ +[ + { + "pk": "1", + "model": "m2m_through_regress.person", + "fields": { + "name": "Guido" + } + }, + { + "pk": "1", + "model": "auth.user", + "fields": { + "username": "Guido", + "email": "bdfl@python.org", + "password": "abcde" + } + }, + { + "pk": "1", + "model": "m2m_through_regress.group", + "fields": { + "name": "Python Core Group" + } + }, + { + "pk": "1", + "model": "m2m_through_regress.usermembership", + "fields": { + "user": "1", + "group": "1", + "price": "100" + } + } +] \ No newline at end of file diff --git a/tests/regressiontests/m2m_through_regress/models.py b/tests/regressiontests/m2m_through_regress/models.py index 8594f19dee..dcf5f8115b 100644 --- a/tests/regressiontests/m2m_through_regress/models.py +++ b/tests/regressiontests/m2m_through_regress/models.py @@ -12,7 +12,9 @@ class Membership(models.Model): def __unicode__(self): return "%s is a member of %s" % (self.person.name, self.group.name) +# using custom id column to test ticket #11107 class UserMembership(models.Model): + id = models.AutoField(db_column='usermembership_id', primary_key=True) user = models.ForeignKey(User) group = models.ForeignKey('Group') price = models.IntegerField(default=100) @@ -196,4 +198,12 @@ doing a join. # Flush the database, just to make sure we can. >>> management.call_command('flush', verbosity=0, interactive=False) +## Regression test for #11107 +Ensure that sequences on m2m_through tables are being created for the through +model, not for a phantom auto-generated m2m table. + +>>> management.call_command('loaddata', 'm2m_through', verbosity=0) +>>> management.call_command('dumpdata', 'm2m_through_regress', format='json') +[{"pk": 1, "model": "m2m_through_regress.usermembership", "fields": {"price": 100, "group": 1, "user": 1}}, {"pk": 1, "model": "m2m_through_regress.person", "fields": {"name": "Guido"}}, {"pk": 1, "model": "m2m_through_regress.group", "fields": {"name": "Python Core Group"}}] + """}