Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[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
  • Loading branch information...
commit 08ab082480f1beaf8dac5cddd0948a1c26f574d8 1 parent 94e002c
Alex Gaynor authored July 16, 2009
1  AUTHORS
@@ -131,6 +131,7 @@ answer newbie questions, and generally made Django that much better:
131 131
     Andrew Durdin <adurdin@gmail.com>
132 132
     dusk@woofle.net
133 133
     Andy Dustman <farcepest@gmail.com>
  134
+    J. Clifford Dyer <jcd@unc.edu>
134 135
     Clint Ecker
135 136
     Nick Efford <nick@efford.org>
136 137
     eibaan@gmail.com
26  django/contrib/admin/sites.py
@@ -114,20 +114,20 @@ def add_action(self, action, name=None):
114 114
         name = name or action.__name__
115 115
         self._actions[name] = action
116 116
         self._global_actions[name] = action
117  
-        
  117
+
118 118
     def disable_action(self, name):
119 119
         """
120 120
         Disable a globally-registered action. Raises KeyError for invalid names.
121 121
         """
122 122
         del self._actions[name]
123  
-        
  123
+
124 124
     def get_action(self, name):
125 125
         """
126 126
         Explicitally get a registered global action wheather it's enabled or
127 127
         not. Raises KeyError for invalid names.
128 128
         """
129 129
         return self._global_actions[name]
130  
-    
  130
+
131 131
     def actions(self):
132 132
         """
133 133
         Get all the enabled actions as an iterable of (name, func).
@@ -159,9 +159,9 @@ def check_dependencies(self):
159 159
         if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
160 160
             raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.")
161 161
 
162  
-    def admin_view(self, view):
  162
+    def admin_view(self, view, cacheable=False):
163 163
         """
164  
-        Decorator to create an "admin view attached to this ``AdminSite``. This
  164
+        Decorator to create an admin view attached to this ``AdminSite``. This
165 165
         wraps the view and provides permission checking by calling
166 166
         ``self.has_permission``.
167 167
 
@@ -177,19 +177,25 @@ def get_urls(self):
177 177
                         url(r'^my_view/$', self.admin_view(some_view))
178 178
                     )
179 179
                     return urls
  180
+
  181
+        By default, admin_views are marked non-cacheable using the
  182
+        ``never_cache`` decorator. If the view can be safely cached, set
  183
+        cacheable=True.
180 184
         """
181 185
         def inner(request, *args, **kwargs):
182 186
             if not self.has_permission(request):
183 187
                 return self.login(request)
184 188
             return view(request, *args, **kwargs)
  189
+        if not cacheable:
  190
+            inner = never_cache(inner)
185 191
         return update_wrapper(inner, view)
186 192
 
187 193
     def get_urls(self):
188 194
         from django.conf.urls.defaults import patterns, url, include
189 195
 
190  
-        def wrap(view):
  196
+        def wrap(view, cacheable=False):
191 197
             def wrapper(*args, **kwargs):
192  
-                return self.admin_view(view)(*args, **kwargs)
  198
+                return self.admin_view(view, cacheable)(*args, **kwargs)
193 199
             return update_wrapper(wrapper, view)
194 200
 
195 201
         # Admin-site-wide views.
@@ -201,13 +207,13 @@ def wrapper(*args, **kwargs):
201 207
                 wrap(self.logout),
202 208
                 name='%sadmin_logout'),
203 209
             url(r'^password_change/$',
204  
-                wrap(self.password_change),
  210
+                wrap(self.password_change, cacheable=True),
205 211
                 name='%sadmin_password_change' % self.name),
206 212
             url(r'^password_change/done/$',
207  
-                wrap(self.password_change_done),
  213
+                wrap(self.password_change_done, cacheable=True),
208 214
                 name='%sadmin_password_change_done' % self.name),
209 215
             url(r'^jsi18n/$',
210  
-                wrap(self.i18n_javascript),
  216
+                wrap(self.i18n_javascript, cacheable=True),
211 217
                 name='%sadmin_jsi18n' % self.name),
212 218
             url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$',
213 219
                 'django.views.defaults.shortcut'),
3  django/contrib/gis/db/models/manager.py
@@ -19,6 +19,9 @@ def area(self, *args, **kwargs):
19 19
     def centroid(self, *args, **kwargs):
20 20
         return self.get_query_set().centroid(*args, **kwargs)
21 21
 
  22
+    def collect(self, *args, **kwargs):
  23
+        return self.get_query_set().collect(*args, **kwargs)
  24
+
22 25
     def difference(self, *args, **kwargs):
23 26
         return self.get_query_set().difference(*args, **kwargs)
24 27
 
8  django/contrib/gis/db/models/query.py
@@ -62,6 +62,14 @@ def centroid(self, **kwargs):
62 62
         """
63 63
         return self._geom_attribute('centroid', **kwargs)
64 64
 
  65
+    def collect(self, **kwargs):
  66
+        """
  67
+        Performs an aggregate collect operation on the given geometry field.
  68
+        This is analagous to a union operation, but much faster because
  69
+        boundaries are not dissolved.
  70
+        """
  71
+        return self._spatial_aggregate(aggregates.Collect, **kwargs)
  72
+
65 73
     def difference(self, geom, **kwargs):
66 74
         """
67 75
         Returns the spatial difference of the geographic field in a `difference`
27  django/contrib/gis/tests/relatedapp/tests.py
... ...
@@ -1,7 +1,7 @@
1 1
 import os, unittest
2 2
 from django.contrib.gis.geos import *
3 3
 from django.contrib.gis.db.backend import SpatialBackend
4  
-from django.contrib.gis.db.models import Count, Extent, F, Union
  4
+from django.contrib.gis.db.models import Collect, Count, Extent, F, Union
5 5
 from django.contrib.gis.tests.utils import no_mysql, no_oracle, no_spatialite
6 6
 from django.conf import settings
7 7
 from models import City, Location, DirectoryEntry, Parcel, Book, Author
@@ -237,7 +237,7 @@ def test12_count(self):
237 237
         # as Dallas.
238 238
         dallas = City.objects.get(name='Dallas')
239 239
         ftworth = City.objects.create(name='Fort Worth', state='TX', location=dallas.location)
240  
-        
  240
+
241 241
         # Count annotation should be 2 for the Dallas location now.
242 242
         loc = Location.objects.annotate(num_cities=Count('city')).get(id=dallas.location.id)
243 243
         self.assertEqual(2, loc.num_cities)
@@ -250,7 +250,7 @@ def test12_count(self):
250 250
         Book.objects.create(title='Blank Spots on the Map', author=tp)
251 251
         wp = Author.objects.create(name='William Patry')
252 252
         Book.objects.create(title='Patry on Copyright', author=wp)
253  
-        
  253
+
254 254
         # Should only be one author (Trevor Paglen) returned by this query, and
255 255
         # the annotation should have 3 for the number of books.
256 256
         qs = Author.objects.annotate(num_books=Count('books')).filter(num_books__gt=1)
@@ -264,6 +264,27 @@ def test13_select_related_null_fk(self):
264 264
         # Should be `None`, and not a 'dummy' model.
265 265
         self.assertEqual(None, b.author)
266 266
 
  267
+    @no_mysql
  268
+    @no_oracle
  269
+    @no_spatialite
  270
+    def test14_collect(self):
  271
+        "Testing the `collect` GeoQuerySet method and `Collect` aggregate."
  272
+        # Reference query:
  273
+        # SELECT AsText(ST_Collect("relatedapp_location"."point")) FROM "relatedapp_city" LEFT OUTER JOIN
  274
+        #    "relatedapp_location" ON ("relatedapp_city"."location_id" = "relatedapp_location"."id")
  275
+        #    WHERE "relatedapp_city"."state" = 'TX';
  276
+        ref_geom = fromstr('MULTIPOINT(-97.516111 33.058333,-96.801611 32.782057,-95.363151 29.763374,-96.801611 32.782057)')
  277
+
  278
+        c1 = City.objects.filter(state='TX').collect(field_name='location__point')
  279
+        c2 = City.objects.filter(state='TX').aggregate(Collect('location__point'))['location__point__collect']
  280
+
  281
+        for coll in (c1, c2):
  282
+            # Even though Dallas and Ft. Worth share same point, Collect doesn't
  283
+            # consolidate -- that's why 4 points in MultiPoint.
  284
+            self.assertEqual(4, len(coll))
  285
+            self.assertEqual(ref_geom, coll)
  286
+
  287
+
267 288
     # TODO: Related tests for KML, GML, and distance lookups.
268 289
 
269 290
 def suite():
13  django/db/backends/oracle/base.py
@@ -217,12 +217,13 @@ def sequence_reset_sql(self, style, model_list):
217 217
                     # continue to loop
218 218
                     break
219 219
             for f in model._meta.many_to_many:
220  
-                table_name = self.quote_name(f.m2m_db_table())
221  
-                sequence_name = get_sequence_name(f.m2m_db_table())
222  
-                column_name = self.quote_name('id')
223  
-                output.append(query % {'sequence': sequence_name,
224  
-                                       'table': table_name,
225  
-                                       'column': column_name})
  220
+                if not f.rel.through:
  221
+                    table_name = self.quote_name(f.m2m_db_table())
  222
+                    sequence_name = get_sequence_name(f.m2m_db_table())
  223
+                    column_name = self.quote_name('id')
  224
+                    output.append(query % {'sequence': sequence_name,
  225
+                                           'table': table_name,
  226
+                                           'column': column_name})
226 227
         return output
227 228
 
228 229
     def start_transaction_sql(self):
17  django/db/backends/postgresql/operations.py
@@ -121,14 +121,15 @@ def sequence_reset_sql(self, style, model_list):
121 121
                         style.SQL_TABLE(qn(model._meta.db_table))))
122 122
                     break # Only one AutoField is allowed per model, so don't bother continuing.
123 123
             for f in model._meta.many_to_many:
124  
-                output.append("%s setval('%s', coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \
125  
-                    (style.SQL_KEYWORD('SELECT'),
126  
-                    style.SQL_FIELD(qn('%s_id_seq' % f.m2m_db_table())),
127  
-                    style.SQL_FIELD(qn('id')),
128  
-                    style.SQL_FIELD(qn('id')),
129  
-                    style.SQL_KEYWORD('IS NOT'),
130  
-                    style.SQL_KEYWORD('FROM'),
131  
-                    style.SQL_TABLE(qn(f.m2m_db_table()))))
  124
+                if not f.rel.through:
  125
+                    output.append("%s setval('%s', coalesce(max(%s), 1), max(%s) %s null) %s %s;" % \
  126
+                        (style.SQL_KEYWORD('SELECT'),
  127
+                        style.SQL_FIELD(qn('%s_id_seq' % f.m2m_db_table())),
  128
+                        style.SQL_FIELD(qn('id')),
  129
+                        style.SQL_FIELD(qn('id')),
  130
+                        style.SQL_KEYWORD('IS NOT'),
  131
+                        style.SQL_KEYWORD('FROM'),
  132
+                        style.SQL_TABLE(qn(f.m2m_db_table()))))
132 133
         return output
133 134
 
134 135
     def savepoint_create_sql(self, sid):
2  docs/howto/custom-model-fields.txt
@@ -464,7 +464,7 @@ should raise either a ``ValueError`` if the ``value`` is of the wrong sort (a
464 464
 list when you were expecting an object, for example) or a ``TypeError`` if
465 465
 your field does not support that type of lookup. For many fields, you can get
466 466
 by with handling the lookup types that need special handling for your field
467  
-and pass the rest of the :meth:`get_db_prep_lookup` method of the parent class.
  467
+and pass the rest to the :meth:`get_db_prep_lookup` method of the parent class.
468 468
 
469 469
 If you needed to implement ``get_db_prep_save()``, you will usually need to
470 470
 implement ``get_db_prep_lookup()``. If you don't, ``get_db_prep_value`` will be
10  docs/howto/error-reporting.txt
@@ -23,6 +23,10 @@ administrators immediate notification of any errors. The :setting:`ADMINS` will
23 23
 get a description of the error, a complete Python traceback, and details about
24 24
 the HTTP request that caused the error.
25 25
 
  26
+By default, Django will send email from root@localhost. However, some mail
  27
+providers reject all email from this address. To use a different sender
  28
+address, modify the :setting:`SERVER_EMAIL` setting.
  29
+
26 30
 To disable this behavior, just remove all entries from the :setting:`ADMINS`
27 31
 setting.
28 32
 
@@ -33,12 +37,12 @@ Django can also be configured to email errors about broken links (404 "page
33 37
 not found" errors). Django sends emails about 404 errors when:
34 38
 
35 39
     * :setting:`DEBUG` is ``False``
36  
-    
  40
+
37 41
     * :setting:`SEND_BROKEN_LINK_EMAILS` is ``True``
38  
-    
  42
+
39 43
     * Your :setting:`MIDDLEWARE_CLASSES` setting includes ``CommonMiddleware``
40 44
       (which it does by default).
41  
-    
  45
+
42 46
 If those conditions are met, Django will e-mail the users listed in the
43 47
 :setting:`MANAGERS` setting whenever your code raises a 404 and the request has
44 48
 a referer. (It doesn't bother to e-mail for 404s that don't have a referer --
2  docs/intro/tutorial03.txt
@@ -365,7 +365,7 @@ That takes care of setting ``handler404`` in the current module. As you can see
365 365
 in ``django/conf/urls/defaults.py``, ``handler404`` is set to
366 366
 :func:`django.views.defaults.page_not_found` by default.
367 367
 
368  
-Three more things to note about 404 views:
  368
+Four more things to note about 404 views:
369 369
 
370 370
     * If :setting:`DEBUG` is set to ``True`` (in your settings module) then your
371 371
       404 view will never be used (and thus the ``404.html`` template will never
BIN  docs/ref/contrib/admin/_images/article_actions.png
30  docs/ref/contrib/admin/index.txt
@@ -762,12 +762,19 @@ documented in :ref:`topics-http-urls`::
762 762
     anything, so you'll usually want to prepend your custom URLs to the built-in
763 763
     ones.
764 764
 
765  
-Note, however, that the ``self.my_view`` function registered above will *not*
766  
-have any permission check done; it'll be accessible to the general public. Since
767  
-this is usually not what you want, Django provides a convience wrapper to check
768  
-permissions. This wrapper is :meth:`AdminSite.admin_view` (i.e.
769  
-``self.admin_site.admin_view`` inside a ``ModelAdmin`` instance); use it like
770  
-so::
  765
+However, the ``self.my_view`` function registered above suffers from two
  766
+problems:
  767
+
  768
+  * It will *not* perform and permission checks, so it will be accessible to
  769
+    the general public.
  770
+  * It will *not* provide any header details to prevent caching. This means if
  771
+    the page retrieves data from the database, and caching middleware is
  772
+    active, the page could show outdated information.
  773
+
  774
+Since this is usually not what you want, Django provides a convenience wrapper
  775
+to check permissions and mark the view as non-cacheable. This wrapper is
  776
+:meth:`AdminSite.admin_view` (i.e.  ``self.admin_site.admin_view`` inside a
  777
+``ModelAdmin`` instance); use it like so::
771 778
 
772 779
     class MyModelAdmin(admin.ModelAdmin):
773 780
         def get_urls(self):
@@ -781,7 +788,14 @@ Notice the wrapped view in the fifth line above::
781 788
 
782 789
     (r'^my_view/$', self.admin_site.admin_view(self.my_view))
783 790
 
784  
-This wrapping will protect ``self.my_view`` from unauthorized access.
  791
+This wrapping will protect ``self.my_view`` from unauthorized access and will
  792
+apply the ``django.views.decorators.cache.never_cache`` decorator to make sure
  793
+it is not cached if the cache middleware is active.
  794
+
  795
+If the page is cacheable, but you still want the permission check to be performed,
  796
+you can pass a ``cacheable=True`` argument to :meth:`AdminSite.admin_view`::
  797
+
  798
+    (r'^my_view/$', self.admin_site.admin_view(self.my_view, cacheable=True))
785 799
 
786 800
 .. method:: ModelAdmin.formfield_for_foreignkey(self, db_field, request, **kwargs)
787 801
 
@@ -849,7 +863,7 @@ provided some extra mapping data that would not otherwise be available::
849 863
                 'osm_data': self.get_osm_info(),
850 864
             }
851 865
             return super(MyModelAdmin, self).change_view(request, object_id,
852  
-                extra_context=my_context))
  866
+                extra_context=my_context)
853 867
 
854 868
 ``ModelAdmin`` media definitions
855 869
 --------------------------------
4  docs/ref/contrib/contenttypes.txt
@@ -177,9 +177,9 @@ The ``ContentTypeManager``
177 177
     .. method:: models.ContentTypeManager.clear_cache()
178 178
 
179 179
         Clears an internal cache used by
180  
-        :class:`~django.contrib.contenttypes.models.ContentType>` to keep track
  180
+        :class:`~django.contrib.contenttypes.models.ContentType` to keep track
181 181
         of which models for which it has created
182  
-        :class:`django.contrib.contenttypes.models.ContentType>` instances. You
  182
+        :class:`django.contrib.contenttypes.models.ContentType` instances. You
183 183
         probably won't ever need to call this method yourself; Django will call
184 184
         it automatically when it's needed.
185 185
 
43  docs/ref/forms/fields.txt
@@ -275,7 +275,7 @@ For each field, we describe the default widget used if you don't specify
275 275
     * Default widget: ``CheckboxInput``
276 276
     * Empty value: ``False``
277 277
     * Normalizes to: A Python ``True`` or ``False`` value.
278  
-    * Validates that the check box is checked (i.e. the value is ``True``) if
  278
+    * Validates that the value is ``True`` (e.g. the check box is checked) if
279 279
       the field has ``required=True``.
280 280
     * Error message keys: ``required``
281 281
 
@@ -287,9 +287,10 @@ For each field, we describe the default widget used if you don't specify
287 287
 .. note::
288 288
 
289 289
     Since all ``Field`` subclasses have ``required=True`` by default, the
290  
-    validation condition here is important. If you want to include a checkbox
291  
-    in your form that can be either checked or unchecked, you must remember to
292  
-    pass in ``required=False`` when creating the ``BooleanField``.
  290
+    validation condition here is important. If you want to include a boolean
  291
+    in your form that can be either ``True`` or ``False`` (e.g. a checked or
  292
+    unchecked checkbox), you must remember to pass in ``required=False`` when
  293
+    creating the ``BooleanField``.
293 294
 
294 295
 ``CharField``
295 296
 ~~~~~~~~~~~~~
@@ -328,7 +329,7 @@ Takes one extra required argument:
328 329
 
329 330
     An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this
330 331
     field.
331  
-    
  332
+
332 333
 ``TypedChoiceField``
333 334
 ~~~~~~~~~~~~~~~~~~~~
334 335
 
@@ -437,7 +438,7 @@ If no ``input_formats`` argument is provided, the default input formats are::
437 438
       ``min_value``, ``max_digits``, ``max_decimal_places``,
438 439
       ``max_whole_digits``
439 440
 
440  
-Takes four optional arguments: 
  441
+Takes four optional arguments:
441 442
 
442 443
 .. attribute:: DecimalField.max_value
443 444
 .. attribute:: DecimalField.min_value
@@ -449,7 +450,7 @@ Takes four optional arguments:
449 450
     The maximum number of digits (those before the decimal point plus those
450 451
     after the decimal point, with leading zeros stripped) permitted in the
451 452
     value.
452  
-    
  453
+
453 454
 .. attribute:: DecimalField.decimal_places
454 455
 
455 456
     The maximum number of decimal places permitted.
@@ -522,18 +523,18 @@ extra arguments; only ``path`` is required:
522 523
     A regular expression pattern; only files with names matching this expression
523 524
     will be allowed as choices.
524 525
 
525  
-``FloatField`` 
526  
-~~~~~~~~~~~~~~ 
527  
-
528  
-    * Default widget: ``TextInput`` 
529  
-    * Empty value: ``None`` 
530  
-    * Normalizes to: A Python float. 
531  
-    * Validates that the given value is an float. Leading and trailing 
532  
-      whitespace is allowed, as in Python's ``float()`` function. 
533  
-    * Error message keys: ``required``, ``invalid``, ``max_value``, 
534  
-      ``min_value`` 
535  
-	 
536  
-Takes two optional arguments for validation, ``max_value`` and ``min_value``. 
  526
+``FloatField``
  527
+~~~~~~~~~~~~~~
  528
+
  529
+    * Default widget: ``TextInput``
  530
+    * Empty value: ``None``
  531
+    * Normalizes to: A Python float.
  532
+    * Validates that the given value is an float. Leading and trailing
  533
+      whitespace is allowed, as in Python's ``float()`` function.
  534
+    * Error message keys: ``required``, ``invalid``, ``max_value``,
  535
+      ``min_value``
  536
+
  537
+Takes two optional arguments for validation, ``max_value`` and ``min_value``.
537 538
 These control the range of values permitted in the field.
538 539
 
539 540
 ``ImageField``
@@ -779,10 +780,10 @@ example::
779 780
    (which is ``"---------"`` by default) with the ``empty_label`` attribute, or
780 781
    you can disable the empty label entirely by setting ``empty_label`` to
781 782
    ``None``::
782  
-   
  783
+
783 784
         # A custom empty label
784 785
         field1 = forms.ModelChoiceField(queryset=..., empty_label="(Nothing)")
785  
-        
  786
+
786 787
         # No empty label
787 788
         field2 = forms.ModelChoiceField(queryset=..., empty_label=None)
788 789
 
2  docs/ref/models/querysets.txt
@@ -668,7 +668,7 @@ of the arguments is required, but you should use at least one of them.
668 668
 
669 669
     The resulting SQL of the above example would be::
670 670
 
671  
-        SELECT blog_blog.*, (SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id)
  671
+        SELECT blog_blog.*, (SELECT COUNT(*) FROM blog_entry WHERE blog_entry.blog_id = blog_blog.id) AS entry_count
672 672
         FROM blog_blog;
673 673
 
674 674
     Note that the parenthesis required by most database engines around
49  docs/topics/forms/formsets.txt
@@ -86,9 +86,9 @@ displayed.
86 86
 Formset validation
87 87
 ------------------
88 88
 
89  
-Validation with a formset is about identical to a regular ``Form``. There is
  89
+Validation with a formset is almost identical to a regular ``Form``. There is
90 90
 an ``is_valid`` method on the formset to provide a convenient way to validate
91  
-each form in the formset::
  91
+all forms in the formset::
92 92
 
93 93
     >>> ArticleFormSet = formset_factory(ArticleForm)
94 94
     >>> formset = ArticleFormSet({})
@@ -97,22 +97,25 @@ each form in the formset::
97 97
 
98 98
 We passed in no data to the formset which is resulting in a valid form. The
99 99
 formset is smart enough to ignore extra forms that were not changed. If we
100  
-attempt to provide an article, but fail to do so::
  100
+provide an invalid article::
101 101
 
102 102
     >>> data = {
103  
-    ...     'form-TOTAL_FORMS': u'1',
104  
-    ...     'form-INITIAL_FORMS': u'1',
  103
+    ...     'form-TOTAL_FORMS': u'2',
  104
+    ...     'form-INITIAL_FORMS': u'0',
105 105
     ...     'form-0-title': u'Test',
106  
-    ...     'form-0-pub_date': u'',
  106
+    ...     'form-0-pub_date': u'16 June 1904',
  107
+    ...     'form-1-title': u'Test',
  108
+    ...     'form-1-pub_date': u'', # <-- this date is missing but required
107 109
     ... }
108 110
     >>> formset = ArticleFormSet(data)
109 111
     >>> formset.is_valid()
110 112
     False
111 113
     >>> formset.errors
112  
-    [{'pub_date': [u'This field is required.']}]
  114
+    [{}, {'pub_date': [u'This field is required.']}]
113 115
 
114  
-As we can see the formset properly performed validation and gave us the
115  
-expected errors.
  116
+As we can see, ``formset.errors`` is a list whose entries correspond to the
  117
+forms in the formset. Validation was performed for each of the two forms, and
  118
+the expected error message appears for the second item.
116 119
 
117 120
 .. _understanding-the-managementform:
118 121
 
@@ -155,20 +158,40 @@ Custom formset validation
155 158
 ~~~~~~~~~~~~~~~~~~~~~~~~~
156 159
 
157 160
 A formset has a ``clean`` method similar to the one on a ``Form`` class. This
158  
-is where you define your own validation that deals at the formset level::
  161
+is where you define your own validation that works at the formset level::
159 162
 
160 163
     >>> from django.forms.formsets import BaseFormSet
161 164
 
162 165
     >>> class BaseArticleFormSet(BaseFormSet):
163 166
     ...     def clean(self):
164  
-    ...         raise forms.ValidationError, u'An error occured.'
  167
+    ...         """Checks that no two articles have the same title."""
  168
+    ...         if any(self.errors):
  169
+    ...             # Don't bother validating the formset unless each form is valid on its own
  170
+    ...             return
  171
+    ...         titles = []
  172
+    ...         for i in range(0, self.total_form_count()):
  173
+    ...             form = self.forms[i]
  174
+    ...             title = form.cleaned_data['title']
  175
+    ...             if title in titles:
  176
+    ...                 raise forms.ValidationError, "Articles in a set must have distinct titles."
  177
+    ...             titles.append(title)
165 178
 
166 179
     >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet)
167  
-    >>> formset = ArticleFormSet({})
  180
+    >>> data = {
  181
+    ...     'form-TOTAL_FORMS': u'2',
  182
+    ...     'form-INITIAL_FORMS': u'0',
  183
+    ...     'form-0-title': u'Test',
  184
+    ...     'form-0-pub_date': u'16 June 1904',
  185
+    ...     'form-1-title': u'Test',
  186
+    ...     'form-1-pub_date': u'23 June 1912',
  187
+    ... }
  188
+    >>> formset = ArticleFormSet(data)
168 189
     >>> formset.is_valid()
169 190
     False
  191
+    >>> formset.errors
  192
+    [{}, {}]
170 193
     >>> formset.non_form_errors()
171  
-    [u'An error occured.']
  194
+    [u'Articles in a set must have distinct titles.']
172 195
 
173 196
 The formset ``clean`` method is called after all the ``Form.clean`` methods
174 197
 have been called. The errors will be found using the ``non_form_errors()``
36  docs/topics/http/urls.txt
@@ -40,14 +40,14 @@ algorithm the system follows to determine which Python code to execute:
40 40
        this is the value of the ``ROOT_URLCONF`` setting, but if the incoming
41 41
        ``HttpRequest`` object has an attribute called ``urlconf``, its value
42 42
        will be used in place of the ``ROOT_URLCONF`` setting.
43  
-    
  43
+
44 44
     2. Django loads that Python module and looks for the variable
45 45
        ``urlpatterns``. This should be a Python list, in the format returned by
46 46
        the function ``django.conf.urls.defaults.patterns()``.
47  
-    
  47
+
48 48
     3. Django runs through each URL pattern, in order, and stops at the first
49 49
        one that matches the requested URL.
50  
-    
  50
+
51 51
     4. Once one of the regexes matches, Django imports and calls the given
52 52
        view, which is a simple Python function. The view gets passed an
53 53
        :class:`~django.http.HttpRequest` as its first argument and any values
@@ -263,8 +263,15 @@ value should suffice.
263 263
 include
264 264
 -------
265 265
 
266  
-A function that takes a full Python import path to another URLconf that should
267  
-be "included" in this place. See `Including other URLconfs`_ below.
  266
+A function that takes a full Python import path to another URLconf module that
  267
+should be "included" in this place.
  268
+
  269
+.. versionadded:: 1.1
  270
+
  271
+:meth:``include`` also accepts as an argument an iterable that returns URL
  272
+patterns.
  273
+
  274
+See `Including other URLconfs`_ below.
268 275
 
269 276
 Notes on capturing text in URLs
270 277
 ===============================
@@ -391,6 +398,25 @@ Django encounters ``include()``, it chops off whatever part of the URL matched
391 398
 up to that point and sends the remaining string to the included URLconf for
392 399
 further processing.
393 400
 
  401
+.. versionadded:: 1.1
  402
+
  403
+Another posibility is to include additional URL patterns not by specifying the
  404
+URLconf Python module defining them as the `include`_ argument but by using
  405
+directly the pattern list as returned by `patterns`_ instead. For example::
  406
+
  407
+    from django.conf.urls.defaults import *
  408
+
  409
+    extra_patterns = patterns('',
  410
+        url(r'reports/(?P<id>\d+)/$', 'credit.views.report', name='credit-reports'),
  411
+        url(r'charge/$', 'credit.views.charge', name='credit-charge'),
  412
+    )
  413
+
  414
+    urlpatterns = patterns('',
  415
+        url(r'^$',    'apps.main.views.homepage', name='site-homepage'),
  416
+        (r'^help/',   include('apps.help.urls')),
  417
+        (r'^credit/', include(extra_patterns)),
  418
+    )
  419
+
394 420
 .. _`Django Web site`: http://www.djangoproject.com/
395 421
 
396 422
 Captured parameters
8  docs/topics/i18n.txt
@@ -959,11 +959,11 @@ Using the JavaScript translation catalog
959 959
 
960 960
 To use the catalog, just pull in the dynamically generated script like this::
961 961
 
962  
-    <script type="text/javascript" src="/path/to/jsi18n/"></script>
  962
+    <script type="text/javascript" src="{% url django.views.i18n.javascript_catalog %}"></script>
963 963
 
964  
-This is how the admin fetches the translation catalog from the server. When the
965  
-catalog is loaded, your JavaScript code can use the standard ``gettext``
966  
-interface to access it::
  964
+This uses reverse URL lookup to find the URL of the JavaScript catalog view.
  965
+When the catalog is loaded, your JavaScript code can use the standard
  966
+``gettext`` interface to access it::
967 967
 
968 968
     document.write(gettext('this is to be translated'));
969 969
 
73  tests/regressiontests/admin_views/tests.py
@@ -10,6 +10,7 @@
10 10
 from django.contrib.admin.sites import LOGIN_FORM_KEY
11 11
 from django.contrib.admin.util import quote
12 12
 from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
  13
+from django.utils.cache import get_max_age
13 14
 from django.utils.html import escape
14 15
 
15 16
 # local test models
@@ -1527,3 +1528,75 @@ def test_ordered_inline(self):
1527 1528
         self.failUnlessEqual(Category.objects.get(id=2).order, 13)
1528 1529
         self.failUnlessEqual(Category.objects.get(id=3).order, 1)
1529 1530
         self.failUnlessEqual(Category.objects.get(id=4).order, 0)
  1531
+
  1532
+
  1533
+class NeverCacheTests(TestCase):
  1534
+    fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml']
  1535
+
  1536
+    def setUp(self):
  1537
+        self.client.login(username='super', password='secret')
  1538
+
  1539
+    def tearDown(self):
  1540
+        self.client.logout()
  1541
+
  1542
+    def testAdminIndex(self):
  1543
+        "Check the never-cache status of the main index"
  1544
+        response = self.client.get('/test_admin/admin/')
  1545
+        self.failUnlessEqual(get_max_age(response), 0)
  1546
+
  1547
+    def testAppIndex(self):
  1548
+        "Check the never-cache status of an application index"
  1549
+        response = self.client.get('/test_admin/admin/admin_views/')
  1550
+        self.failUnlessEqual(get_max_age(response), 0)
  1551
+
  1552
+    def testModelIndex(self):
  1553
+        "Check the never-cache status of a model index"
  1554
+        response = self.client.get('/test_admin/admin/admin_views/fabric/')
  1555
+        self.failUnlessEqual(get_max_age(response), 0)
  1556
+
  1557
+    def testModelAdd(self):
  1558
+        "Check the never-cache status of a model add page"
  1559
+        response = self.client.get('/test_admin/admin/admin_views/fabric/add/')
  1560
+        self.failUnlessEqual(get_max_age(response), 0)
  1561
+
  1562
+    def testModelView(self):
  1563
+        "Check the never-cache status of a model edit page"
  1564
+        response = self.client.get('/test_admin/admin/admin_views/section/1/')
  1565
+        self.failUnlessEqual(get_max_age(response), 0)
  1566
+
  1567
+    def testModelHistory(self):
  1568
+        "Check the never-cache status of a model history page"
  1569
+        response = self.client.get('/test_admin/admin/admin_views/section/1/history/')
  1570
+        self.failUnlessEqual(get_max_age(response), 0)
  1571
+
  1572
+    def testModelDelete(self):
  1573
+        "Check the never-cache status of a model delete page"
  1574
+        response = self.client.get('/test_admin/admin/admin_views/section/1/delete/')
  1575
+        self.failUnlessEqual(get_max_age(response), 0)
  1576
+
  1577
+    def testLogin(self):
  1578
+        "Check the never-cache status of login views"
  1579
+        self.client.logout()
  1580
+        response = self.client.get('/test_admin/admin/')
  1581
+        self.failUnlessEqual(get_max_age(response), 0)
  1582
+
  1583
+    def testLogout(self):
  1584
+        "Check the never-cache status of logout view"
  1585
+        response = self.client.get('/test_admin/admin/logout/')
  1586
+        self.failUnlessEqual(get_max_age(response), 0)
  1587
+
  1588
+    def testPasswordChange(self):
  1589
+        "Check the never-cache status of the password change view"
  1590
+        self.client.logout()
  1591
+        response = self.client.get('/test_admin/password_change/')
  1592
+        self.failUnlessEqual(get_max_age(response), None)
  1593
+
  1594
+    def testPasswordChangeDone(self):
  1595
+        "Check the never-cache status of the password change done view"
  1596
+        response = self.client.get('/test_admin/admin/password_change/done/')
  1597
+        self.failUnlessEqual(get_max_age(response), None)
  1598
+
  1599
+    def testJsi18n(self):
  1600
+        "Check the never-cache status of the Javascript i18n view"
  1601
+        response = self.client.get('/test_admin/jsi18n/')
  1602
+        self.failUnlessEqual(get_max_age(response), None)
34  tests/regressiontests/m2m_through_regress/fixtures/m2m_through.json
... ...
@@ -0,0 +1,34 @@
  1
+[
  2
+    {
  3
+        "pk": "1",
  4
+        "model": "m2m_through_regress.person",
  5
+        "fields": {
  6
+            "name": "Guido"
  7
+        }
  8
+    },
  9
+    {
  10
+        "pk": "1",
  11
+        "model": "auth.user",
  12
+        "fields": {
  13
+             "username": "Guido",
  14
+             "email": "bdfl@python.org",
  15
+             "password": "abcde"
  16
+        }
  17
+    },
  18
+    {
  19
+        "pk": "1",
  20
+        "model": "m2m_through_regress.group",
  21
+        "fields": {
  22
+            "name": "Python Core Group"
  23
+        }
  24
+    },
  25
+    {
  26
+        "pk": "1",
  27
+        "model": "m2m_through_regress.usermembership",
  28
+        "fields": {
  29
+            "user": "1",
  30
+            "group": "1",
  31
+            "price": "100"
  32
+        }
  33
+    }
  34
+]
10  tests/regressiontests/m2m_through_regress/models.py
@@ -12,7 +12,9 @@ class Membership(models.Model):
12 12
     def __unicode__(self):
13 13
         return "%s is a member of %s" % (self.person.name, self.group.name)
14 14
 
  15
+# using custom id column to test ticket #11107
15 16
 class UserMembership(models.Model):
  17
+    id = models.AutoField(db_column='usermembership_id', primary_key=True)
16 18
     user = models.ForeignKey(User)
17 19
     group = models.ForeignKey('Group')
18 20
     price = models.IntegerField(default=100)
@@ -196,4 +198,12 @@ class B(models.Model):
196 198
 # Flush the database, just to make sure we can.
197 199
 >>> management.call_command('flush', verbosity=0, interactive=False)
198 200
 
  201
+## Regression test for #11107
  202
+Ensure that sequences on m2m_through tables are being created for the through
  203
+model, not for a phantom auto-generated m2m table.
  204
+
  205
+>>> management.call_command('loaddata', 'm2m_through', verbosity=0)
  206
+>>> management.call_command('dumpdata', 'm2m_through_regress', format='json')
  207
+[{"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"}}]
  208
+
199 209
 """}

0 notes on commit 08ab082

Please sign in to comment.
Something went wrong with that request. Please try again.