mirror of
https://github.com/django/django.git
synced 2025-07-05 18:29:11 +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:
parent
94e002c6e4
commit
08ab082480
1
AUTHORS
1
AUTHORS
@ -131,6 +131,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Andrew Durdin <adurdin@gmail.com>
|
Andrew Durdin <adurdin@gmail.com>
|
||||||
dusk@woofle.net
|
dusk@woofle.net
|
||||||
Andy Dustman <farcepest@gmail.com>
|
Andy Dustman <farcepest@gmail.com>
|
||||||
|
J. Clifford Dyer <jcd@unc.edu>
|
||||||
Clint Ecker
|
Clint Ecker
|
||||||
Nick Efford <nick@efford.org>
|
Nick Efford <nick@efford.org>
|
||||||
eibaan@gmail.com
|
eibaan@gmail.com
|
||||||
|
@ -114,20 +114,20 @@ class AdminSite(object):
|
|||||||
name = name or action.__name__
|
name = name or action.__name__
|
||||||
self._actions[name] = action
|
self._actions[name] = action
|
||||||
self._global_actions[name] = action
|
self._global_actions[name] = action
|
||||||
|
|
||||||
def disable_action(self, name):
|
def disable_action(self, name):
|
||||||
"""
|
"""
|
||||||
Disable a globally-registered action. Raises KeyError for invalid names.
|
Disable a globally-registered action. Raises KeyError for invalid names.
|
||||||
"""
|
"""
|
||||||
del self._actions[name]
|
del self._actions[name]
|
||||||
|
|
||||||
def get_action(self, name):
|
def get_action(self, name):
|
||||||
"""
|
"""
|
||||||
Explicitally get a registered global action wheather it's enabled or
|
Explicitally get a registered global action wheather it's enabled or
|
||||||
not. Raises KeyError for invalid names.
|
not. Raises KeyError for invalid names.
|
||||||
"""
|
"""
|
||||||
return self._global_actions[name]
|
return self._global_actions[name]
|
||||||
|
|
||||||
def actions(self):
|
def actions(self):
|
||||||
"""
|
"""
|
||||||
Get all the enabled actions as an iterable of (name, func).
|
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:
|
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.")
|
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
|
wraps the view and provides permission checking by calling
|
||||||
``self.has_permission``.
|
``self.has_permission``.
|
||||||
|
|
||||||
@ -177,19 +177,25 @@ class AdminSite(object):
|
|||||||
url(r'^my_view/$', self.admin_view(some_view))
|
url(r'^my_view/$', self.admin_view(some_view))
|
||||||
)
|
)
|
||||||
return urls
|
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):
|
def inner(request, *args, **kwargs):
|
||||||
if not self.has_permission(request):
|
if not self.has_permission(request):
|
||||||
return self.login(request)
|
return self.login(request)
|
||||||
return view(request, *args, **kwargs)
|
return view(request, *args, **kwargs)
|
||||||
|
if not cacheable:
|
||||||
|
inner = never_cache(inner)
|
||||||
return update_wrapper(inner, view)
|
return update_wrapper(inner, view)
|
||||||
|
|
||||||
def get_urls(self):
|
def get_urls(self):
|
||||||
from django.conf.urls.defaults import patterns, url, include
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
|
|
||||||
def wrap(view):
|
def wrap(view, cacheable=False):
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
return self.admin_view(view)(*args, **kwargs)
|
return self.admin_view(view, cacheable)(*args, **kwargs)
|
||||||
return update_wrapper(wrapper, view)
|
return update_wrapper(wrapper, view)
|
||||||
|
|
||||||
# Admin-site-wide views.
|
# Admin-site-wide views.
|
||||||
@ -201,13 +207,13 @@ class AdminSite(object):
|
|||||||
wrap(self.logout),
|
wrap(self.logout),
|
||||||
name='%sadmin_logout'),
|
name='%sadmin_logout'),
|
||||||
url(r'^password_change/$',
|
url(r'^password_change/$',
|
||||||
wrap(self.password_change),
|
wrap(self.password_change, cacheable=True),
|
||||||
name='%sadmin_password_change' % self.name),
|
name='%sadmin_password_change' % self.name),
|
||||||
url(r'^password_change/done/$',
|
url(r'^password_change/done/$',
|
||||||
wrap(self.password_change_done),
|
wrap(self.password_change_done, cacheable=True),
|
||||||
name='%sadmin_password_change_done' % self.name),
|
name='%sadmin_password_change_done' % self.name),
|
||||||
url(r'^jsi18n/$',
|
url(r'^jsi18n/$',
|
||||||
wrap(self.i18n_javascript),
|
wrap(self.i18n_javascript, cacheable=True),
|
||||||
name='%sadmin_jsi18n' % self.name),
|
name='%sadmin_jsi18n' % self.name),
|
||||||
url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$',
|
url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$',
|
||||||
'django.views.defaults.shortcut'),
|
'django.views.defaults.shortcut'),
|
||||||
|
@ -19,6 +19,9 @@ class GeoManager(Manager):
|
|||||||
def centroid(self, *args, **kwargs):
|
def centroid(self, *args, **kwargs):
|
||||||
return self.get_query_set().centroid(*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):
|
def difference(self, *args, **kwargs):
|
||||||
return self.get_query_set().difference(*args, **kwargs)
|
return self.get_query_set().difference(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -62,6 +62,14 @@ class GeoQuerySet(QuerySet):
|
|||||||
"""
|
"""
|
||||||
return self._geom_attribute('centroid', **kwargs)
|
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):
|
def difference(self, geom, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns the spatial difference of the geographic field in a `difference`
|
Returns the spatial difference of the geographic field in a `difference`
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import os, unittest
|
import os, unittest
|
||||||
from django.contrib.gis.geos import *
|
from django.contrib.gis.geos import *
|
||||||
from django.contrib.gis.db.backend import SpatialBackend
|
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.contrib.gis.tests.utils import no_mysql, no_oracle, no_spatialite
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from models import City, Location, DirectoryEntry, Parcel, Book, Author
|
from models import City, Location, DirectoryEntry, Parcel, Book, Author
|
||||||
@ -237,7 +237,7 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||||||
# as Dallas.
|
# as Dallas.
|
||||||
dallas = City.objects.get(name='Dallas')
|
dallas = City.objects.get(name='Dallas')
|
||||||
ftworth = City.objects.create(name='Fort Worth', state='TX', location=dallas.location)
|
ftworth = City.objects.create(name='Fort Worth', state='TX', location=dallas.location)
|
||||||
|
|
||||||
# Count annotation should be 2 for the Dallas location now.
|
# Count annotation should be 2 for the Dallas location now.
|
||||||
loc = Location.objects.annotate(num_cities=Count('city')).get(id=dallas.location.id)
|
loc = Location.objects.annotate(num_cities=Count('city')).get(id=dallas.location.id)
|
||||||
self.assertEqual(2, loc.num_cities)
|
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)
|
Book.objects.create(title='Blank Spots on the Map', author=tp)
|
||||||
wp = Author.objects.create(name='William Patry')
|
wp = Author.objects.create(name='William Patry')
|
||||||
Book.objects.create(title='Patry on Copyright', author=wp)
|
Book.objects.create(title='Patry on Copyright', author=wp)
|
||||||
|
|
||||||
# Should only be one author (Trevor Paglen) returned by this query, and
|
# Should only be one author (Trevor Paglen) returned by this query, and
|
||||||
# the annotation should have 3 for the number of books.
|
# the annotation should have 3 for the number of books.
|
||||||
qs = Author.objects.annotate(num_books=Count('books')).filter(num_books__gt=1)
|
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.
|
# Should be `None`, and not a 'dummy' model.
|
||||||
self.assertEqual(None, b.author)
|
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.
|
# TODO: Related tests for KML, GML, and distance lookups.
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
|
@ -217,12 +217,13 @@ WHEN (new.%(col_name)s IS NULL)
|
|||||||
# continue to loop
|
# continue to loop
|
||||||
break
|
break
|
||||||
for f in model._meta.many_to_many:
|
for f in model._meta.many_to_many:
|
||||||
table_name = self.quote_name(f.m2m_db_table())
|
if not f.rel.through:
|
||||||
sequence_name = get_sequence_name(f.m2m_db_table())
|
table_name = self.quote_name(f.m2m_db_table())
|
||||||
column_name = self.quote_name('id')
|
sequence_name = get_sequence_name(f.m2m_db_table())
|
||||||
output.append(query % {'sequence': sequence_name,
|
column_name = self.quote_name('id')
|
||||||
'table': table_name,
|
output.append(query % {'sequence': sequence_name,
|
||||||
'column': column_name})
|
'table': table_name,
|
||||||
|
'column': column_name})
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def start_transaction_sql(self):
|
def start_transaction_sql(self):
|
||||||
|
@ -121,14 +121,15 @@ class DatabaseOperations(BaseDatabaseOperations):
|
|||||||
style.SQL_TABLE(qn(model._meta.db_table))))
|
style.SQL_TABLE(qn(model._meta.db_table))))
|
||||||
break # Only one AutoField is allowed per model, so don't bother continuing.
|
break # Only one AutoField is allowed per model, so don't bother continuing.
|
||||||
for f in model._meta.many_to_many:
|
for f in model._meta.many_to_many:
|
||||||
output.append("%s setval('%s', coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \
|
if not f.rel.through:
|
||||||
(style.SQL_KEYWORD('SELECT'),
|
output.append("%s setval('%s', coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \
|
||||||
style.SQL_FIELD(qn('%s_id_seq' % f.m2m_db_table())),
|
(style.SQL_KEYWORD('SELECT'),
|
||||||
style.SQL_FIELD(qn('id')),
|
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_FIELD(qn('id')),
|
||||||
style.SQL_KEYWORD('FROM'),
|
style.SQL_KEYWORD('IS NOT'),
|
||||||
style.SQL_TABLE(qn(f.m2m_db_table()))))
|
style.SQL_KEYWORD('FROM'),
|
||||||
|
style.SQL_TABLE(qn(f.m2m_db_table()))))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def savepoint_create_sql(self, sid):
|
def savepoint_create_sql(self, sid):
|
||||||
|
@ -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
|
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
|
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
|
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
|
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
|
implement ``get_db_prep_lookup()``. If you don't, ``get_db_prep_value`` will be
|
||||||
|
@ -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
|
get a description of the error, a complete Python traceback, and details about
|
||||||
the HTTP request that caused the error.
|
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`
|
To disable this behavior, just remove all entries from the :setting:`ADMINS`
|
||||||
setting.
|
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:
|
not found" errors). Django sends emails about 404 errors when:
|
||||||
|
|
||||||
* :setting:`DEBUG` is ``False``
|
* :setting:`DEBUG` is ``False``
|
||||||
|
|
||||||
* :setting:`SEND_BROKEN_LINK_EMAILS` is ``True``
|
* :setting:`SEND_BROKEN_LINK_EMAILS` is ``True``
|
||||||
|
|
||||||
* Your :setting:`MIDDLEWARE_CLASSES` setting includes ``CommonMiddleware``
|
* Your :setting:`MIDDLEWARE_CLASSES` setting includes ``CommonMiddleware``
|
||||||
(which it does by default).
|
(which it does by default).
|
||||||
|
|
||||||
If those conditions are met, Django will e-mail the users listed in the
|
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
|
: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 --
|
a referer. (It doesn't bother to e-mail for 404s that don't have a referer --
|
||||||
|
@ -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
|
in ``django/conf/urls/defaults.py``, ``handler404`` is set to
|
||||||
:func:`django.views.defaults.page_not_found` by default.
|
: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
|
* 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
|
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 |
@ -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
|
anything, so you'll usually want to prepend your custom URLs to the built-in
|
||||||
ones.
|
ones.
|
||||||
|
|
||||||
Note, however, that the ``self.my_view`` function registered above will *not*
|
However, the ``self.my_view`` function registered above suffers from two
|
||||||
have any permission check done; it'll be accessible to the general public. Since
|
problems:
|
||||||
this is usually not what you want, Django provides a convience wrapper to check
|
|
||||||
permissions. This wrapper is :meth:`AdminSite.admin_view` (i.e.
|
* It will *not* perform and permission checks, so it will be accessible to
|
||||||
``self.admin_site.admin_view`` inside a ``ModelAdmin`` instance); use it like
|
the general public.
|
||||||
so::
|
* 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):
|
class MyModelAdmin(admin.ModelAdmin):
|
||||||
def get_urls(self):
|
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))
|
(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)
|
.. 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(),
|
'osm_data': self.get_osm_info(),
|
||||||
}
|
}
|
||||||
return super(MyModelAdmin, self).change_view(request, object_id,
|
return super(MyModelAdmin, self).change_view(request, object_id,
|
||||||
extra_context=my_context))
|
extra_context=my_context)
|
||||||
|
|
||||||
``ModelAdmin`` media definitions
|
``ModelAdmin`` media definitions
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
@ -177,9 +177,9 @@ The ``ContentTypeManager``
|
|||||||
.. method:: models.ContentTypeManager.clear_cache()
|
.. method:: models.ContentTypeManager.clear_cache()
|
||||||
|
|
||||||
Clears an internal cache used by
|
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
|
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
|
probably won't ever need to call this method yourself; Django will call
|
||||||
it automatically when it's needed.
|
it automatically when it's needed.
|
||||||
|
|
||||||
|
@ -275,7 +275,7 @@ For each field, we describe the default widget used if you don't specify
|
|||||||
* Default widget: ``CheckboxInput``
|
* Default widget: ``CheckboxInput``
|
||||||
* Empty value: ``False``
|
* Empty value: ``False``
|
||||||
* Normalizes to: A Python ``True`` or ``False`` value.
|
* 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``.
|
the field has ``required=True``.
|
||||||
* Error message keys: ``required``
|
* Error message keys: ``required``
|
||||||
|
|
||||||
@ -287,9 +287,10 @@ For each field, we describe the default widget used if you don't specify
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Since all ``Field`` subclasses have ``required=True`` by default, the
|
Since all ``Field`` subclasses have ``required=True`` by default, the
|
||||||
validation condition here is important. If you want to include a checkbox
|
validation condition here is important. If you want to include a boolean
|
||||||
in your form that can be either checked or unchecked, you must remember to
|
in your form that can be either ``True`` or ``False`` (e.g. a checked or
|
||||||
pass in ``required=False`` when creating the ``BooleanField``.
|
unchecked checkbox), you must remember to pass in ``required=False`` when
|
||||||
|
creating the ``BooleanField``.
|
||||||
|
|
||||||
``CharField``
|
``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
|
An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this
|
||||||
field.
|
field.
|
||||||
|
|
||||||
``TypedChoiceField``
|
``TypedChoiceField``
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@ -437,7 +438,7 @@ If no ``input_formats`` argument is provided, the default input formats are::
|
|||||||
``min_value``, ``max_digits``, ``max_decimal_places``,
|
``min_value``, ``max_digits``, ``max_decimal_places``,
|
||||||
``max_whole_digits``
|
``max_whole_digits``
|
||||||
|
|
||||||
Takes four optional arguments:
|
Takes four optional arguments:
|
||||||
|
|
||||||
.. attribute:: DecimalField.max_value
|
.. attribute:: DecimalField.max_value
|
||||||
.. attribute:: DecimalField.min_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
|
The maximum number of digits (those before the decimal point plus those
|
||||||
after the decimal point, with leading zeros stripped) permitted in the
|
after the decimal point, with leading zeros stripped) permitted in the
|
||||||
value.
|
value.
|
||||||
|
|
||||||
.. attribute:: DecimalField.decimal_places
|
.. attribute:: DecimalField.decimal_places
|
||||||
|
|
||||||
The maximum number of decimal places permitted.
|
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
|
A regular expression pattern; only files with names matching this expression
|
||||||
will be allowed as choices.
|
will be allowed as choices.
|
||||||
|
|
||||||
``FloatField``
|
``FloatField``
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* Default widget: ``TextInput``
|
* Default widget: ``TextInput``
|
||||||
* Empty value: ``None``
|
* Empty value: ``None``
|
||||||
* Normalizes to: A Python float.
|
* Normalizes to: A Python float.
|
||||||
* Validates that the given value is an float. Leading and trailing
|
* Validates that the given value is an float. Leading and trailing
|
||||||
whitespace is allowed, as in Python's ``float()`` function.
|
whitespace is allowed, as in Python's ``float()`` function.
|
||||||
* Error message keys: ``required``, ``invalid``, ``max_value``,
|
* Error message keys: ``required``, ``invalid``, ``max_value``,
|
||||||
``min_value``
|
``min_value``
|
||||||
|
|
||||||
Takes two optional arguments for validation, ``max_value`` and ``min_value``.
|
Takes two optional arguments for validation, ``max_value`` and ``min_value``.
|
||||||
These control the range of values permitted in the field.
|
These control the range of values permitted in the field.
|
||||||
|
|
||||||
``ImageField``
|
``ImageField``
|
||||||
@ -779,10 +780,10 @@ example::
|
|||||||
(which is ``"---------"`` by default) with the ``empty_label`` attribute, or
|
(which is ``"---------"`` by default) with the ``empty_label`` attribute, or
|
||||||
you can disable the empty label entirely by setting ``empty_label`` to
|
you can disable the empty label entirely by setting ``empty_label`` to
|
||||||
``None``::
|
``None``::
|
||||||
|
|
||||||
# A custom empty label
|
# A custom empty label
|
||||||
field1 = forms.ModelChoiceField(queryset=..., empty_label="(Nothing)")
|
field1 = forms.ModelChoiceField(queryset=..., empty_label="(Nothing)")
|
||||||
|
|
||||||
# No empty label
|
# No empty label
|
||||||
field2 = forms.ModelChoiceField(queryset=..., empty_label=None)
|
field2 = forms.ModelChoiceField(queryset=..., empty_label=None)
|
||||||
|
|
||||||
|
@ -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::
|
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;
|
FROM blog_blog;
|
||||||
|
|
||||||
Note that the parenthesis required by most database engines around
|
Note that the parenthesis required by most database engines around
|
||||||
|
@ -86,9 +86,9 @@ displayed.
|
|||||||
Formset validation
|
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
|
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)
|
>>> ArticleFormSet = formset_factory(ArticleForm)
|
||||||
>>> formset = ArticleFormSet({})
|
>>> 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
|
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
|
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 = {
|
>>> data = {
|
||||||
... 'form-TOTAL_FORMS': u'1',
|
... 'form-TOTAL_FORMS': u'2',
|
||||||
... 'form-INITIAL_FORMS': u'1',
|
... 'form-INITIAL_FORMS': u'0',
|
||||||
... 'form-0-title': u'Test',
|
... '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 = ArticleFormSet(data)
|
||||||
>>> formset.is_valid()
|
>>> formset.is_valid()
|
||||||
False
|
False
|
||||||
>>> formset.errors
|
>>> 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
|
As we can see, ``formset.errors`` is a list whose entries correspond to the
|
||||||
expected errors.
|
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:
|
.. _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
|
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
|
>>> from django.forms.formsets import BaseFormSet
|
||||||
|
|
||||||
>>> class BaseArticleFormSet(BaseFormSet):
|
>>> class BaseArticleFormSet(BaseFormSet):
|
||||||
... def clean(self):
|
... 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)
|
>>> 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()
|
>>> formset.is_valid()
|
||||||
False
|
False
|
||||||
|
>>> formset.errors
|
||||||
|
[{}, {}]
|
||||||
>>> formset.non_form_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
|
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()``
|
have been called. The errors will be found using the ``non_form_errors()``
|
||||||
|
@ -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
|
this is the value of the ``ROOT_URLCONF`` setting, but if the incoming
|
||||||
``HttpRequest`` object has an attribute called ``urlconf``, its value
|
``HttpRequest`` object has an attribute called ``urlconf``, its value
|
||||||
will be used in place of the ``ROOT_URLCONF`` setting.
|
will be used in place of the ``ROOT_URLCONF`` setting.
|
||||||
|
|
||||||
2. Django loads that Python module and looks for the variable
|
2. Django loads that Python module and looks for the variable
|
||||||
``urlpatterns``. This should be a Python list, in the format returned by
|
``urlpatterns``. This should be a Python list, in the format returned by
|
||||||
the function ``django.conf.urls.defaults.patterns()``.
|
the function ``django.conf.urls.defaults.patterns()``.
|
||||||
|
|
||||||
3. Django runs through each URL pattern, in order, and stops at the first
|
3. Django runs through each URL pattern, in order, and stops at the first
|
||||||
one that matches the requested URL.
|
one that matches the requested URL.
|
||||||
|
|
||||||
4. Once one of the regexes matches, Django imports and calls the given
|
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
|
view, which is a simple Python function. The view gets passed an
|
||||||
:class:`~django.http.HttpRequest` as its first argument and any values
|
:class:`~django.http.HttpRequest` as its first argument and any values
|
||||||
@ -263,8 +263,15 @@ value should suffice.
|
|||||||
include
|
include
|
||||||
-------
|
-------
|
||||||
|
|
||||||
A function that takes a full Python import path to another URLconf that should
|
A function that takes a full Python import path to another URLconf module that
|
||||||
be "included" in this place. See `Including other URLconfs`_ below.
|
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
|
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
|
up to that point and sends the remaining string to the included URLconf for
|
||||||
further processing.
|
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/
|
.. _`Django Web site`: http://www.djangoproject.com/
|
||||||
|
|
||||||
Captured parameters
|
Captured parameters
|
||||||
|
@ -959,11 +959,11 @@ Using the JavaScript translation catalog
|
|||||||
|
|
||||||
To use the catalog, just pull in the dynamically generated script like this::
|
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
|
This uses reverse URL lookup to find the URL of the JavaScript catalog view.
|
||||||
catalog is loaded, your JavaScript code can use the standard ``gettext``
|
When the catalog is loaded, your JavaScript code can use the standard
|
||||||
interface to access it::
|
``gettext`` interface to access it::
|
||||||
|
|
||||||
document.write(gettext('this is to be translated'));
|
document.write(gettext('this is to be translated'));
|
||||||
|
|
||||||
|
@ -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.sites import LOGIN_FORM_KEY
|
||||||
from django.contrib.admin.util import quote
|
from django.contrib.admin.util import quote
|
||||||
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||||
|
from django.utils.cache import get_max_age
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
|
|
||||||
# local test models
|
# 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=2).order, 13)
|
||||||
self.failUnlessEqual(Category.objects.get(id=3).order, 1)
|
self.failUnlessEqual(Category.objects.get(id=3).order, 1)
|
||||||
self.failUnlessEqual(Category.objects.get(id=4).order, 0)
|
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)
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -12,7 +12,9 @@ class Membership(models.Model):
|
|||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return "%s is a member of %s" % (self.person.name, self.group.name)
|
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):
|
class UserMembership(models.Model):
|
||||||
|
id = models.AutoField(db_column='usermembership_id', primary_key=True)
|
||||||
user = models.ForeignKey(User)
|
user = models.ForeignKey(User)
|
||||||
group = models.ForeignKey('Group')
|
group = models.ForeignKey('Group')
|
||||||
price = models.IntegerField(default=100)
|
price = models.IntegerField(default=100)
|
||||||
@ -196,4 +198,12 @@ doing a join.
|
|||||||
# Flush the database, just to make sure we can.
|
# Flush the database, just to make sure we can.
|
||||||
>>> management.call_command('flush', verbosity=0, interactive=False)
|
>>> 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"}}]
|
||||||
|
|
||||||
"""}
|
"""}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user