1
0
mirror of https://github.com/django/django.git synced 2025-07-05 10:19:20 +00:00

[soc2009/multidb] Merged up to trunk r11240.

git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11247 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Alex Gaynor 2009-07-16 03:02:08 +00:00
parent 94e002c6e4
commit 08ab082480
21 changed files with 311 additions and 85 deletions

View File

@ -131,6 +131,7 @@ answer newbie questions, and generally made Django that much better:
Andrew Durdin <adurdin@gmail.com>
dusk@woofle.net
Andy Dustman <farcepest@gmail.com>
J. Clifford Dyer <jcd@unc.edu>
Clint Ecker
Nick Efford <nick@efford.org>
eibaan@gmail.com

View File

@ -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<content_type_id>\d+)/(?P<object_id>.+)/$',
'django.views.defaults.shortcut'),

View File

@ -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)

View File

@ -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`

View File

@ -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():

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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 --

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -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
--------------------------------

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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()``

View File

@ -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<id>\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

View File

@ -959,11 +959,11 @@ Using the JavaScript translation catalog
To use the catalog, just pull in the dynamically generated script like this::
<script type="text/javascript" src="/path/to/jsi18n/"></script>
<script type="text/javascript" src="{% url django.views.i18n.javascript_catalog %}"></script>
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'));

View File

@ -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)

View File

@ -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"
}
}
]

View File

@ -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"}}]
"""}