From 0b0741602b18928a418ba4661dc24b880daa5253 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Sat, 18 May 2013 12:32:48 +0200 Subject: Fixed #20294 -- Documented context processors in TemplateResponseMixin. --- docs/ref/class-based-views/mixins-simple.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'docs/ref') diff --git a/docs/ref/class-based-views/mixins-simple.txt b/docs/ref/class-based-views/mixins-simple.txt index 6796675529..377c85cc3b 100644 --- a/docs/ref/class-based-views/mixins-simple.txt +++ b/docs/ref/class-based-views/mixins-simple.txt @@ -60,6 +60,17 @@ TemplateResponseMixin altered later (e.g. in :ref:`template response middleware `). + .. admonition:: Context processors + + ``TemplateResponse`` uses :class:`~django.template.RequestContext` + which means that callables defined in + :setting:`TEMPLATE_CONTEXT_PROCESSORS` may overwrite template + variables defined in your views. For example, if you subclass + :class:`DetailView ` and + set ``context_object_name`` to ``user``, the + ``django.contrib.auth.context_processors.auth`` context processor + will happily overwrite your variable with current user. + If you need custom template loading or custom context object instantiation, create a ``TemplateResponse`` subclass and assign it to ``response_class``. -- cgit v1.3 From 7b85ef9dfb83bd2f2cde46b9836b9fd12a033b26 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 12:45:06 +0200 Subject: Fixed #20408 -- Clarified that values_list() doesn't return a list. Thanks marktranchant, bmispelon, and alextreme. --- docs/ref/models/querysets.txt | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'docs/ref') diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index ffada19082..14123cd79a 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -544,6 +544,11 @@ It is an error to pass in ``flat`` when there is more than one field. If you don't pass any values to ``values_list()``, it will return all the fields in the model, in the order they were declared. +Note that this method returns a ``ValuesListQuerySet``. This class behaves +like a list. Most of the time this is enough, but if you require an actual +Python list object, you can simply call ``list()`` on it, which will evaluate +the queryset. + dates ~~~~~ -- cgit v1.3 From 756b81dbd1a947351670b66c7e91116abe6aa5c2 Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sat, 18 May 2013 14:13:00 +0200 Subject: Fixed #13546 -- Easier handling of localize field options in ModelForm --- django/forms/models.py | 37 ++++++++++++++++++++++------------- docs/ref/forms/models.txt | 16 +++++++++------ docs/releases/1.6.txt | 4 ++++ docs/topics/forms/modelforms.txt | 36 ++++++++++++++++++++++++++++++++++ tests/model_forms_regress/tests.py | 35 +++++++++++++++++++++++++++++++++ tests/model_formsets_regress/tests.py | 7 ++++++- 6 files changed, 114 insertions(+), 21 deletions(-) (limited to 'docs/ref') diff --git a/django/forms/models.py b/django/forms/models.py index af5cda8faf..6b61560cca 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -136,7 +136,7 @@ def model_to_dict(instance, fields=None, exclude=None): data[f.name] = f.value_from_object(instance) return data -def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None): +def fields_for_model(model, fields=None, exclude=None, widgets=None, localized_fields=None, formfield_callback=None): """ Returns a ``SortedDict`` containing form fields for the given model. @@ -162,10 +162,12 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c continue if exclude and f.name in exclude: continue + + kwargs = {} if widgets and f.name in widgets: - kwargs = {'widget': widgets[f.name]} - else: - kwargs = {} + kwargs['widget'] = widgets[f.name] + if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields): + kwargs['localize'] = True if formfield_callback is None: formfield = f.formfield(**kwargs) @@ -192,6 +194,7 @@ class ModelFormOptions(object): self.fields = getattr(options, 'fields', None) self.exclude = getattr(options, 'exclude', None) self.widgets = getattr(options, 'widgets', None) + self.localized_fields = getattr(options, 'localized_fields', None) class ModelFormMetaclass(type): @@ -215,7 +218,7 @@ class ModelFormMetaclass(type): # We check if a string was passed to `fields` or `exclude`, # which is likely to be a mistake where the user typed ('foo') instead # of ('foo',) - for opt in ['fields', 'exclude']: + for opt in ['fields', 'exclude', 'localized_fields']: value = getattr(opts, opt) if isinstance(value, six.string_types) and value != ALL_FIELDS: msg = ("%(model)s.Meta.%(opt)s cannot be a string. " @@ -242,8 +245,9 @@ class ModelFormMetaclass(type): # fields from the model" opts.fields = None - fields = fields_for_model(opts.model, opts.fields, - opts.exclude, opts.widgets, formfield_callback) + fields = fields_for_model(opts.model, opts.fields, opts.exclude, + opts.widgets, opts.localized_fields, formfield_callback) + # make sure opts.fields doesn't specify an invalid field none_model_fields = [k for k, v in six.iteritems(fields) if not v] missing_fields = set(none_model_fields) - \ @@ -409,7 +413,7 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)): pass def modelform_factory(model, form=ModelForm, fields=None, exclude=None, - formfield_callback=None, widgets=None): + localized_fields=None, widgets=None, formfield_callback=None): """ Returns a ModelForm containing form fields for the given model. @@ -423,6 +427,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, ``widgets`` is a dictionary of model field names mapped to a widget. + ``localized_fields`` is a list of names of fields which should be localized. + ``formfield_callback`` is a callable that takes a model field and returns a form field. """ @@ -438,6 +444,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, attrs['exclude'] = exclude if widgets is not None: attrs['widgets'] = widgets + if localized_fields is not None: + attrs['localized_fields'] = localized_fields # If parent form class already has an inner Meta, the Meta we're # creating needs to inherit from the parent's inner meta. @@ -726,8 +734,8 @@ class BaseModelFormSet(BaseFormSet): def modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, - can_order=False, max_num=None, fields=None, - exclude=None, widgets=None, validate_max=False): + can_order=False, max_num=None, fields=None, exclude=None, + widgets=None, validate_max=False, localized_fields=None): """ Returns a FormSet class for the given Django model class. """ @@ -748,7 +756,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None, form = modelform_factory(model, form=form, fields=fields, exclude=exclude, formfield_callback=formfield_callback, - widgets=widgets) + widgets=widgets, localized_fields=localized_fields) FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, can_order=can_order, can_delete=can_delete, validate_max=validate_max) @@ -885,9 +893,9 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): def inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, - fields=None, exclude=None, - extra=3, can_order=False, can_delete=True, max_num=None, - formfield_callback=None, widgets=None, validate_max=False): + fields=None, exclude=None, extra=3, can_order=False, + can_delete=True, max_num=None, formfield_callback=None, + widgets=None, validate_max=False, localized_fields=None): """ Returns an ``InlineFormSet`` for the given kwargs. @@ -910,6 +918,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm, 'max_num': max_num, 'widgets': widgets, 'validate_max': validate_max, + 'localized_fields': localized_fields, } FormSet = modelformset_factory(model, **kwargs) FormSet.fk = fk diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index 9b3480758a..54d34160a5 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -5,7 +5,7 @@ Model Form Functions .. module:: django.forms.models :synopsis: Django's functions for building model forms and formsets. -.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None) +.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None) Returns a :class:`~django.forms.ModelForm` class for the given ``model``. You can optionally pass a ``form`` argument to use as a starting point for @@ -20,6 +20,8 @@ Model Form Functions ``widgets`` is a dictionary of model field names mapped to a widget. + ``localized_fields`` is a list of names of fields which should be localized. + ``formfield_callback`` is a callable that takes a model field and returns a form field. @@ -33,12 +35,14 @@ Model Form Functions information. Omitting any definition of the fields to use will result in all fields being used, but this behaviour is deprecated. -.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False) + The ``localized_fields`` parameter was added. + +.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None) Returns a ``FormSet`` class for the given ``model`` class. Arguments ``model``, ``form``, ``fields``, ``exclude``, - ``formfield_callback`` and ``widgets`` are all passed through to + ``formfield_callback``, ``widgets`` and ``localized_fields`` are all passed through to :func:`~django.forms.models.modelform_factory`. Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, @@ -50,9 +54,9 @@ Model Form Functions .. versionchanged:: 1.6 - The ``widgets`` and the ``validate_max`` parameters were added. + The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. -.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False) +.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None) Returns an ``InlineFormSet`` using :func:`modelformset_factory` with defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and @@ -65,4 +69,4 @@ Model Form Functions .. versionchanged:: 1.6 - The ``widgets`` and the ``validate_max`` parameters were added. + The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 98889254cd..c9b3715c45 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -234,6 +234,10 @@ Minor features .. _`Pillow`: https://pypi.python.org/pypi/Pillow .. _`PIL`: https://pypi.python.org/pypi/PIL +* :doc:`ModelForm ` accepts a new + Meta option: ``localized_fields``. Fields included in this list will be localized + (by setting ``localize`` on the form field). + Backwards incompatible changes in 1.6 ===================================== diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index e58dade736..3cd8c69ab5 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -474,6 +474,24 @@ parameter when declaring the form field:: See the :doc:`form field documentation ` for more information on fields and their arguments. + +Enabling localization of fields +------------------------------- + +.. versionadded:: 1.6 + +By default, the fields in a ``ModelForm`` will not localize their data. To +enable localization for fields, you can use the ``localized_fields`` +attribute on the ``Meta`` class. + + >>> class AuthorForm(ModelForm): + ... class Meta: + ... model = Author + ... localized_fields = ('birth_date',) + +If ``localized_fields`` is set to the special value ``'__all__'``, all fields +will be localized. + .. _overriding-modelform-clean-method: Overriding the clean() method @@ -570,6 +588,10 @@ keyword arguments, or the corresponding attributes on the ``ModelForm`` inner ``Meta`` class. Please see the ``ModelForm`` :ref:`modelforms-selecting-fields` documentation. +... or enable localization for specific fields:: + + >>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=("birth_date",)) + .. _model-formsets: Model formsets @@ -663,6 +685,20 @@ class of a ``ModelForm`` works:: >>> AuthorFormSet = modelformset_factory( ... Author, widgets={'name': Textarea(attrs={'cols': 80, 'rows': 20}) +Enabling localization for fields with ``localized_fields`` +---------------------------------------------------------- + +.. versionadded:: 1.6 + +Using the ``localized_fields`` parameter, you can enable localization for +fields in the form. + + >>> AuthorFormSet = modelformset_factory( + ... Author, localized_fields=('value',)) + +If ``localized_fields`` is set to the special value ``'__all__'``, all fields +will be localized. + Providing initial values ------------------------ diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py index 0e033e033f..80900a59e0 100644 --- a/tests/model_forms_regress/tests.py +++ b/tests/model_forms_regress/tests.py @@ -92,6 +92,41 @@ class OverrideCleanTests(TestCase): self.assertEqual(form.instance.left, 1) + +class PartiallyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = ('left', 'right',) + + +class FullyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = "__all__" + +class LocalizedModelFormTest(TestCase): + def test_model_form_applies_localize_to_some_fields(self): + f = PartiallyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertFalse(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_applies_localize_to_all_fields(self): + f = FullyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertTrue(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_refuses_arbitrary_string(self): + with self.assertRaises(TypeError): + class BrokenLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = "foo" + + # Regression test for #12960. # Make sure the cleaned_data returned from ModelForm.clean() is applied to the # model instance. diff --git a/tests/model_formsets_regress/tests.py b/tests/model_formsets_regress/tests.py index 38ebd9d24b..c8fb1e76c1 100644 --- a/tests/model_formsets_regress/tests.py +++ b/tests/model_formsets_regress/tests.py @@ -273,6 +273,7 @@ class UserSiteForm(forms.ModelForm): 'id': CustomWidget, 'data': CustomWidget, } + localized_fields = ('data',) class Callback(object): @@ -297,19 +298,23 @@ class FormfieldCallbackTests(TestCase): form = Formset().forms[0] self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) + self.assertFalse(form.fields['id'].localize) + self.assertTrue(form.fields['data'].localize) def test_modelformset_factory_default(self): Formset = modelformset_factory(UserSite, form=UserSiteForm) form = Formset().forms[0] self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) + self.assertFalse(form.fields['id'].localize) + self.assertTrue(form.fields['data'].localize) def assertCallbackCalled(self, callback): id_field, user_field, data_field = UserSite._meta.fields expected_log = [ (id_field, {'widget': CustomWidget}), (user_field, {}), - (data_field, {'widget': CustomWidget}), + (data_field, {'widget': CustomWidget, 'localize': True}), ] self.assertEqual(callback.log, expected_log) -- cgit v1.3 From d77082b43877b4df0f29fa977b73b7b4feab84d4 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Sat, 18 May 2013 14:22:40 +0200 Subject: Added example of using sitemaps with static views. References #16829. --- docs/ref/contrib/sitemaps.txt | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'docs/ref') diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index d37ee83378..56a15cb9e0 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -280,6 +280,46 @@ Here's an example of a :doc:`URLconf ` using both:: .. _URLconf: ../url_dispatch/ +Sitemap for static views +======================== + +Often you want the search engine crawlers to index views which are neither +object detail pages nor flatpages. The solution is to explicitly list URL +names for these views in ``items`` and call +:func:`~django.core.urlresolvers.reverse` in the ``location`` method of +the sitemap. For example:: + + # sitemaps.py + from django.contrib import sitemaps + from django.core.urlresolvers import reverse + + class StaticViewSitemap(sitemaps.Sitemap): + priority = 0.5 + changefreq = 'daily' + + def items(self): + return ['main', 'about', 'license'] + + def location(self, item): + return reverse(item) + + # urls.py + from django.conf.urls import patterns, url + from .sitemaps import StaticViewSitemap + + sitemaps = { + 'static': StaticViewSitemap, + } + + urlpatterns = patterns('', + url(r'^$', 'views.main', name='main'), + url(r'^about/$', 'views.about', name='about'), + url(r'^license/$', 'views.license', name='license'), + # ... + url(r'^sitemap\.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}) + ) + + Creating a sitemap index ======================== -- cgit v1.3 From 90f1170bb92cfd588762039fc00477bfcdae11b3 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 18 May 2013 14:24:27 +0200 Subject: Fixed #20269 -- Adapted PostGIS template create script for CentOS/RHEL Thanks Stephane Benchimol for the report and the initial script and mfandreas for the patch. --- docs/ref/contrib/gis/install/create_template_postgis-1.5.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh b/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh index 081b5f2656..67c82a8b25 100755 --- a/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh +++ b/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh @@ -1,9 +1,15 @@ #!/usr/bin/env bash -POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 +if [[ `uname -r | grep el6` ]]; then + POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis + POSTGIS_SQL_FILE=$POSTGIS_SQL_PATH/postgis-64.sql +else + POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 + POSTGIS_SQL_FILE=$POSTGIS_SQL_PATH/postgis.sql +fi createdb -E UTF8 template_postgis # Create the template spatial database. createlang -d template_postgis plpgsql # Adding PLPGSQL language support. psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" -psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql # Loading the PostGIS SQL routines +psql -d template_postgis -f $POSTGIS_SQL_FILE # Loading the PostGIS SQL routines psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" # Enabling users to alter spatial tables. psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" -- cgit v1.3 From a19e9d80ffa10f8da43addcaa4ddd440beee8a4d Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Fri, 17 May 2013 15:31:41 -0400 Subject: Fixed #20430 - Enable iterable of iterables for model choices Allows for any iterable, not just lists or tuples, to be used as the inner item for a list of choices in a model. --- django/core/management/validation.py | 4 ++-- docs/ref/models/fields.txt | 7 ++++--- docs/releases/1.6.txt | 3 +++ tests/invalid_models/invalid_models/models.py | 4 ++-- tests/model_validation/__init__.py | 0 tests/model_validation/models.py | 27 +++++++++++++++++++++++++++ tests/model_validation/tests.py | 14 ++++++++++++++ 7 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 tests/model_validation/__init__.py create mode 100644 tests/model_validation/models.py create mode 100644 tests/model_validation/tests.py (limited to 'docs/ref') diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 0f0eade569..2040a14582 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -118,8 +118,8 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name) else: for c in f.choices: - if not isinstance(c, (list, tuple)) or len(c) != 2: - e.add(opts, '"%s": "choices" should be a sequence of two-tuples.' % f.name) + if isinstance(c, six.string_types) or not is_iterable(c) or len(c) != 2: + e.add(opts, '"%s": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).' % f.name) if f.db_index not in (None, True, False): e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..38b6459909 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -80,9 +80,10 @@ If a field has ``blank=False``, the field will be required. .. attribute:: Field.choices -An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this -field. If this is given, the default form widget will be a select box with -these choices instead of the standard text field. +An iterable (e.g., a list or tuple) consisting itself of iterables of exactly +two items (e.g. ``[(A, B), (A, B) ...]``) to use as choices for this field. If +this is given, the default form widget will be a select box with these choices +instead of the standard text field. The first element in each tuple is the actual value to be stored, and the second element is the human-readable name. For example:: diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 60b3381dd6..b10a9ef81e 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -238,6 +238,9 @@ Minor features Meta option: ``localized_fields``. Fields included in this list will be localized (by setting ``localize`` on the form field). +* The ``choices`` argument to model fields now accepts an iterable of iterables + instead of requiring an iterable of lists or tuples. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/invalid_models/invalid_models/models.py b/tests/invalid_models/invalid_models/models.py index 3c21e1ddb8..6ba3f04e5e 100644 --- a/tests/invalid_models/invalid_models/models.py +++ b/tests/invalid_models/invalid_models/models.py @@ -375,8 +375,8 @@ invalid_models.fielderrors: "decimalfield3": DecimalFields require a "max_digits invalid_models.fielderrors: "decimalfield4": DecimalFields require a "max_digits" attribute value that is greater than or equal to the value of the "decimal_places" attribute. invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute. invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list). -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). invalid_models.fielderrors: "index": "db_index" should be either None, True or False. invalid_models.fielderrors: "field_": Field names cannot end with underscores, because this would lead to ambiguous queryset filters. invalid_models.fielderrors: "nullbool": BooleanFields do not accept null values. Use a NullBooleanField instead. diff --git a/tests/model_validation/__init__.py b/tests/model_validation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/model_validation/models.py b/tests/model_validation/models.py new file mode 100644 index 0000000000..9a2a5f7cd0 --- /dev/null +++ b/tests/model_validation/models.py @@ -0,0 +1,27 @@ +from django.db import models + + +class ThingItem(object): + + def __init__(self, value, display): + self.value = value + self.display = display + + def __iter__(self): + return (x for x in [self.value, self.display]) + + def __len__(self): + return 2 + + +class Things(object): + + def __iter__(self): + return (x for x in [ThingItem(1, 2), ThingItem(3, 4)]) + + +class ThingWithIterableChoices(models.Model): + + # Testing choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + thing = models.CharField(max_length=100, blank=True, choices=Things()) diff --git a/tests/model_validation/tests.py b/tests/model_validation/tests.py new file mode 100644 index 0000000000..96baf640eb --- /dev/null +++ b/tests/model_validation/tests.py @@ -0,0 +1,14 @@ +import io + +from django.core import management +from django.test import TestCase + + +class ModelValidationTest(TestCase): + + def test_models_validate(self): + # All our models should validate properly + # Validation Tests: + # * choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + management.call_command("validate", stdout=io.BytesIO()) -- cgit v1.3 From 56e2f6ccae76a6f0a7f1d64677bf29e11518e5c6 Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sat, 18 May 2013 17:16:07 +0200 Subject: Fixed #20446 -- Documentation for SmallIntegerField does not clarify 'small' --- docs/ref/models/fields.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..da5fe9bf60 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -889,7 +889,8 @@ The value ``0`` is accepted for backward compatibility reasons. .. class:: PositiveSmallIntegerField([**options]) Like a :class:`PositiveIntegerField`, but only allows values under a certain -(database-dependent) point. +(database-dependent) point. Values up to 32767 are safe in all databases +supported by Django. ``SlugField`` ------------- @@ -917,7 +918,8 @@ of some other value. You can do this automatically in the admin using .. class:: SmallIntegerField([**options]) Like an :class:`IntegerField`, but only allows values under a certain -(database-dependent) point. +(database-dependent) point. Values from -32768 to 32767 are safe in all databases +supported by Django. ``TextField`` ------------- -- cgit v1.3 From bd97f7d0cb72191744552142817184e88ce8841d Mon Sep 17 00:00:00 2001 From: Łukasz Langa Date: Sat, 18 May 2013 16:10:14 +0200 Subject: Fixed #15201: Marked CACHE_MIDDLEWARE_ANONYMOUS_ONLY as deprecated --- django/middleware/cache.py | 11 ++++++----- docs/faq/admin.txt | 6 ------ docs/internals/deprecation.txt | 2 ++ docs/ref/settings.txt | 8 ++++++-- docs/releases/1.6.txt | 17 +++++++++++++++++ docs/topics/cache.txt | 12 +++--------- tests/cache/tests.py | 8 +++++--- 7 files changed, 39 insertions(+), 25 deletions(-) (limited to 'docs/ref') diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 83860e15f3..e13a8c3918 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -29,11 +29,6 @@ More details about how the caching works: of the response's "Cache-Control" header, falling back to the CACHE_MIDDLEWARE_SECONDS setting if the section was not found. -* If CACHE_MIDDLEWARE_ANONYMOUS_ONLY is set to True, only anonymous requests - (i.e., those not made by a logged-in user) will be cached. This is a simple - and effective way of avoiding the caching of the Django admin (and any other - user-specific content). - * This middleware expects that a HEAD request is answered with the same response headers exactly like the corresponding GET request. @@ -48,6 +43,8 @@ More details about how the caching works: """ +import warnings + from django.conf import settings from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age @@ -200,5 +197,9 @@ class CacheMiddleware(UpdateCacheMiddleware, FetchFromCacheMiddleware): else: self.cache_anonymous_only = cache_anonymous_only + if self.cache_anonymous_only: + msg = "CACHE_MIDDLEWARE_ANONYMOUS_ONLY has been deprecated and will be removed in Django 1.8." + warnings.warn(msg, PendingDeprecationWarning, stacklevel=1) + self.cache = get_cache(self.cache_alias, **cache_kwargs) self.cache_timeout = self.cache.default_timeout diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index 1d9a7c7427..ec40754094 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -27,12 +27,6 @@ account has :attr:`~django.contrib.auth.models.User.is_active` and :attr:`~django.contrib.auth.models.User.is_staff` set to True. The admin site only allows access to users with those two fields both set to True. -How can I prevent the cache middleware from caching the admin site? -------------------------------------------------------------------- - -Set the :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` setting to ``True``. See the -:doc:`cache documentation ` for more information. - How do I automatically set a field's value to the user who last edited the object in the admin? ----------------------------------------------------------------------------------------------- diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 774de2a2fd..095b6d0a33 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -390,6 +390,8 @@ these changes. ``django.test.testcases.OutputChecker`` will be removed. Instead use the doctest module from the Python standard library. +* The ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting will be removed. + 2.0 --- diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index eb470cdd14..8ef59064f7 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -280,6 +280,12 @@ CACHE_MIDDLEWARE_ANONYMOUS_ONLY Default: ``False`` +.. deprecated:: 1.6 + + This setting was largely ineffective because of using cookies for sessions + and CSRF. See the :doc:`Django 1.6 release notes` for more + information. + If the value of this setting is ``True``, only anonymous requests (i.e., not those made by a logged-in user) will be cached. Otherwise, the middleware caches every page that doesn't have GET or POST parameters. @@ -287,8 +293,6 @@ caches every page that doesn't have GET or POST parameters. If you set the value of this setting to ``True``, you should make sure you've activated ``AuthenticationMiddleware``. -See :doc:`/topics/cache`. - .. setting:: CACHE_MIDDLEWARE_KEY_PREFIX CACHE_MIDDLEWARE_KEY_PREFIX diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index ef79e5770c..f8e1fd6339 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -569,6 +569,23 @@ If necessary, you can temporarily disable auto-escaping with :func:`~django.utils.safestring.mark_safe` or :ttag:`{% autoescape off %} `. +``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``CacheMiddleware`` used to provide a way to cache requests only if they +weren't made by a logged-in user. This mechanism was largely ineffective +because the middleware correctly takes into account the ``Vary: Cookie`` HTTP +header, and this header is being set on a variety of occasions, such as: + +* accessing the session, or +* using CSRF protection, which is turned on by default, or +* using a client-side library which sets cookies, like `Google Analytics`__. + +This makes the cache effectively work on a per-session basis regardless of the +``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting. + +__ http://www.google.com/analytics/ + ``SEND_BROKEN_LINK_EMAILS`` setting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index a7d54fbeb0..46911a593f 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -443,15 +443,9 @@ Then, add the following required settings to your Django settings file: The cache middleware caches GET and HEAD responses with status 200, where the request and response headers allow. Responses to requests for the same URL with different query parameters are considered to be unique pages and are cached separately. -Optionally, if the :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` -setting is ``True``, only anonymous requests (i.e., not those made by a -logged-in user) will be cached. This is a simple and effective way of disabling -caching for any user-specific pages (including Django's admin interface). Note -that if you use :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY`, you should make -sure you've activated ``AuthenticationMiddleware``. The cache middleware -expects that a HEAD request is answered with the same response headers as -the corresponding GET request; in which case it can return a cached GET -response for HEAD request. +The cache middleware expects that a HEAD request is answered with the same +response headers as the corresponding GET request; in which case it can return +a cached GET response for HEAD request. Additionally, the cache middleware automatically sets a few headers in each :class:`~django.http.HttpResponse`: diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 4f7ee8b525..231a3bfb50 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -28,8 +28,8 @@ from django.middleware.cache import (FetchFromCacheMiddleware, from django.template import Template from django.template.response import TemplateResponse from django.test import TestCase, TransactionTestCase, RequestFactory -from django.test.utils import override_settings, six -from django.utils import timezone, translation, unittest +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin +from django.utils import six, timezone, translation, unittest from django.utils.cache import (patch_vary_headers, get_cache_key, learn_cache_key, patch_cache_control, patch_response_headers) from django.utils.encoding import force_text @@ -1592,9 +1592,10 @@ def hello_world_view(request, value): }, }, ) -class CacheMiddlewareTest(TestCase): +class CacheMiddlewareTest(IgnorePendingDeprecationWarningsMixin, TestCase): def setUp(self): + super(CacheMiddlewareTest, self).setUp() self.factory = RequestFactory() self.default_cache = get_cache('default') self.other_cache = get_cache('other') @@ -1602,6 +1603,7 @@ class CacheMiddlewareTest(TestCase): def tearDown(self): self.default_cache.clear() self.other_cache.clear() + super(CacheMiddlewareTest, self).tearDown() def test_constructor(self): """ -- cgit v1.3 From e4591debd19361e628317e936ed8123d9897dd6a Mon Sep 17 00:00:00 2001 From: Marc Egli Date: Sat, 18 May 2013 12:12:26 +0200 Subject: Add missing imports and models to the examples in the the model layer documentation --- docs/ref/models/fields.txt | 10 +++++++++ docs/ref/models/instances.txt | 10 +++++++++ docs/ref/models/options.txt | 6 +++++ docs/ref/models/querysets.txt | 7 ++++++ docs/ref/models/relations.txt | 8 +++++-- docs/topics/db/aggregation.txt | 40 +++++++++++++++++++-------------- docs/topics/db/managers.txt | 22 ++++++++++++------- docs/topics/db/models.txt | 50 ++++++++++++++++++++++++++++++++++++------ docs/topics/db/queries.txt | 2 ++ 9 files changed, 122 insertions(+), 33 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..0ce97c3eaa 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -97,6 +97,8 @@ second element is the human-readable name. For example:: Generally, it's best to define choices inside a model class, and to define a suitably-named constant for each value:: + from django.db import models + class Student(models.Model): FRESHMAN = 'FR' SOPHOMORE = 'SO' @@ -994,12 +996,15 @@ relationship with itself -- use ``models.ForeignKey('self')``. If you need to create a relationship on a model that has not yet been defined, you can use the name of the model, rather than the model object itself:: + from django.db import models + class Car(models.Model): manufacturer = models.ForeignKey('Manufacturer') # ... class Manufacturer(models.Model): # ... + pass To refer to models defined in another application, you can explicitly specify a model with the full application label. For example, if the ``Manufacturer`` @@ -1132,6 +1137,9 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in necessary to avoid executing queries at the time your models.py is imported:: + from django.db import models + from django.contrib.auth.models import User + def get_sentinel_user(): return User.objects.get_or_create(username='deleted')[0] @@ -1204,6 +1212,8 @@ that control how the relationship functions. Only used in the definition of ManyToManyFields on self. Consider the following model:: + from django.db import models + class Person(models.Model): friends = models.ManyToManyField("self") diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index b4b162a9ea..f989ff1bec 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -34,6 +34,8 @@ that, you need to :meth:`~Model.save()`. 1. Add a classmethod on the model class:: + from django.db import models + class Book(models.Model): title = models.CharField(max_length=100) @@ -105,6 +107,7 @@ individually. You'll need to call ``full_clean`` manually when you want to run one-step model validation for your own manually created models. For example:: + from django.core.exceptions import ValidationError try: article.full_clean() except ValidationError as e: @@ -132,6 +135,7 @@ automatically provide a value for a field, or to do validation that requires access to more than a single field:: def clean(self): + import datetime from django.core.exceptions import ValidationError # Don't allow draft entries to have a pub_date. if self.status == 'draft' and self.pub_date is not None: @@ -434,6 +438,8 @@ representation of the model from the ``__unicode__()`` method. For example:: + from django.db import models + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -460,6 +466,9 @@ Thus, you should return a nice, human-readable string for the object's The previous :meth:`~Model.__unicode__()` example could be similarly written using ``__str__()`` like this:: + from django.db import models + from django.utils.encoding import force_bytes + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -490,6 +499,7 @@ function is usually the best approach.) For example:: def get_absolute_url(self): + from django.core.urlresolvers import reverse return reverse('people.views.details', args=[str(self.id)]) One place Django uses ``get_absolute_url()`` is in the admin app. If an object diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 5f9316bd2a..90099d13a3 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -145,6 +145,12 @@ Django quotes column and table names behind the scenes. and a question has more than one answer, and the order of answers matters, you'd do this:: + from django.db import models + + class Question(models.Model): + text = models.TextField() + # ... + class Answer(models.Model): question = models.ForeignKey(Question) # ... diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 14123cd79a..9677b321c6 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -232,6 +232,7 @@ the model field that is being aggregated. For example, if you were manipulating a list of blogs, you may want to determine how many entries have been made in each blog:: + >>> from django.db.models import Count >>> q = Blog.objects.annotate(Count('entry')) # The name of the first blog >>> q[0].name @@ -699,6 +700,8 @@ And here's ``select_related`` lookup:: ``select_related()`` follows foreign keys as far as possible. If you have the following models:: + from django.db import models + class City(models.Model): # ... pass @@ -814,6 +817,8 @@ that are supported by ``select_related``. It also supports prefetching of For example, suppose you have these models:: + from django.db import models + class Topping(models.Model): name = models.CharField(max_length=30) @@ -1565,6 +1570,7 @@ aggregated. For example, when you are working with blog entries, you may want to know the number of authors that have contributed blog entries:: + >>> from django.db.models import Count >>> q = Blog.objects.aggregate(Count('entry')) {'entry__count': 16} @@ -2042,6 +2048,7 @@ Range test (inclusive). Example:: + import datetime start_date = datetime.date(2005, 1, 1) end_date = datetime.date(2005, 3, 31) Entry.objects.filter(pub_date__range=(start_date, end_date)) diff --git a/docs/ref/models/relations.txt b/docs/ref/models/relations.txt index c923961a19..ffebe37193 100644 --- a/docs/ref/models/relations.txt +++ b/docs/ref/models/relations.txt @@ -12,8 +12,11 @@ Related objects reference * The "other side" of a :class:`~django.db.models.ForeignKey` relation. That is:: + from django.db import models + class Reporter(models.Model): - ... + # ... + pass class Article(models.Model): reporter = models.ForeignKey(Reporter) @@ -24,7 +27,8 @@ Related objects reference * Both sides of a :class:`~django.db.models.ManyToManyField` relation:: class Topping(models.Model): - ... + # ... + pass class Pizza(models.Model): toppings = models.ManyToManyField(Topping) diff --git a/docs/topics/db/aggregation.txt b/docs/topics/db/aggregation.txt index 125cd0bdee..1024d6b0c2 100644 --- a/docs/topics/db/aggregation.txt +++ b/docs/topics/db/aggregation.txt @@ -18,27 +18,29 @@ used to track the inventory for a series of online bookstores: .. code-block:: python + from django.db import models + class Author(models.Model): - name = models.CharField(max_length=100) - age = models.IntegerField() + name = models.CharField(max_length=100) + age = models.IntegerField() class Publisher(models.Model): - name = models.CharField(max_length=300) - num_awards = models.IntegerField() + name = models.CharField(max_length=300) + num_awards = models.IntegerField() class Book(models.Model): - name = models.CharField(max_length=300) - pages = models.IntegerField() - price = models.DecimalField(max_digits=10, decimal_places=2) - rating = models.FloatField() - authors = models.ManyToManyField(Author) - publisher = models.ForeignKey(Publisher) - pubdate = models.DateField() + name = models.CharField(max_length=300) + pages = models.IntegerField() + price = models.DecimalField(max_digits=10, decimal_places=2) + rating = models.FloatField() + authors = models.ManyToManyField(Author) + publisher = models.ForeignKey(Publisher) + pubdate = models.DateField() class Store(models.Model): - name = models.CharField(max_length=300) - books = models.ManyToManyField(Book) - registered_users = models.PositiveIntegerField() + name = models.CharField(max_length=300) + books = models.ManyToManyField(Book) + registered_users = models.PositiveIntegerField() Cheat sheet =========== @@ -123,7 +125,7 @@ If you want to generate more than one aggregate, you just add another argument to the ``aggregate()`` clause. So, if we also wanted to know the maximum and minimum price of all books, we would issue the query:: - >>> from django.db.models import Avg, Max, Min, Count + >>> from django.db.models import Avg, Max, Min >>> Book.objects.aggregate(Avg('price'), Max('price'), Min('price')) {'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')} @@ -148,6 +150,7 @@ the number of authors: .. code-block:: python # Build an annotated queryset + >>> from django.db.models import Count >>> q = Book.objects.annotate(Count('authors')) # Interrogate the first object in the queryset >>> q[0] @@ -192,6 +195,7 @@ and aggregate the related value. For example, to find the price range of books offered in each store, you could use the annotation:: + >>> from django.db.models import Max, Min >>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price')) This tells Django to retrieve the ``Store`` model, join (through the @@ -222,7 +226,7 @@ For example, we can ask for all publishers, annotated with their respective total book stock counters (note how we use ``'book'`` to specify the ``Publisher`` -> ``Book`` reverse foreign key hop):: - >>> from django.db.models import Count, Min, Sum, Max, Avg + >>> from django.db.models import Count, Min, Sum, Avg >>> Publisher.objects.annotate(Count('book')) (Every ``Publisher`` in the resulting ``QuerySet`` will have an extra attribute @@ -269,6 +273,7 @@ constraining the objects for which an annotation is calculated. For example, you can generate an annotated list of all books that have a title starting with "Django" using the query:: + >>> from django.db.models import Count, Avg >>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors')) When used with an ``aggregate()`` clause, a filter has the effect of @@ -407,6 +412,8 @@ particularly, when counting things. By way of example, suppose you have a model like this:: + from django.db import models + class Item(models.Model): name = models.CharField(max_length=10) data = models.IntegerField() @@ -457,5 +464,6 @@ For example, if you wanted to calculate the average number of authors per book you first annotate the set of books with the author count, then aggregate that author count, referencing the annotation field:: + >>> from django.db.models import Count, Avg >>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors')) {'num_authors__avg': 1.66} diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index 2a0f7e4ce0..b940b09d33 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -62,6 +62,8 @@ For example, this custom ``Manager`` offers a method ``with_counts()``, which returns a list of all ``OpinionPoll`` objects, each with an extra ``num_responses`` attribute that is the result of an aggregate query:: + from django.db import models + class PollManager(models.Manager): def with_counts(self): from django.db import connection @@ -101,6 +103,8 @@ Modifying initial Manager QuerySets A ``Manager``'s base ``QuerySet`` returns all objects in the system. For example, using this model:: + from django.db import models + class Book(models.Model): title = models.CharField(max_length=100) author = models.CharField(max_length=50) @@ -236,7 +240,7 @@ class, but still customize the default manager. For example, suppose you have this base class:: class AbstractBase(models.Model): - ... + # ... objects = CustomManager() class Meta: @@ -246,14 +250,15 @@ If you use this directly in a subclass, ``objects`` will be the default manager if you declare no managers in the base class:: class ChildA(AbstractBase): - ... + # ... # This class has CustomManager as the default manager. + pass If you want to inherit from ``AbstractBase``, but provide a different default manager, you can provide the default manager on the child class:: class ChildB(AbstractBase): - ... + # ... # An explicit default manager. default_manager = OtherManager() @@ -274,9 +279,10 @@ it into the inheritance hierarchy *after* the defaults:: abstract = True class ChildC(AbstractBase, ExtraManager): - ... + # ... # Default manager is CustomManager, but OtherManager is # also available via the "extra_manager" attribute. + pass Note that while you can *define* a custom manager on the abstract model, you can't *invoke* any methods using the abstract model. That is:: @@ -349,8 +355,7 @@ the manager class:: class MyManager(models.Manager): use_for_related_fields = True - - ... + # ... If this attribute is set on the *default* manager for a model (only the default manager is considered in these situations), Django will use that class @@ -396,7 +401,8 @@ it, whereas the following will not work:: # BAD: Incorrect code class MyManager(models.Manager): - ... + # ... + pass # Sets the attribute on an instance of MyManager. Django will # ignore this setting. @@ -404,7 +410,7 @@ it, whereas the following will not work:: mgr.use_for_related_fields = True class MyModel(models.Model): - ... + # ... objects = mgr # End of incorrect code. diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index dd7714052d..baef01b6fb 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -90,6 +90,8 @@ attributes. Be careful not to choose field names that conflict with the Example:: + from django.db import models + class Musician(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -290,8 +292,11 @@ For example, if a ``Car`` model has a ``Manufacturer`` -- that is, a ``Manufacturer`` makes multiple cars but each ``Car`` only has one ``Manufacturer`` -- use the following definitions:: + from django.db import models + class Manufacturer(models.Model): # ... + pass class Car(models.Model): manufacturer = models.ForeignKey(Manufacturer) @@ -340,8 +345,11 @@ For example, if a ``Pizza`` has multiple ``Topping`` objects -- that is, a ``Topping`` can be on multiple pizzas and each ``Pizza`` has multiple toppings -- here's how you'd represent that:: + from django.db import models + class Topping(models.Model): # ... + pass class Pizza(models.Model): # ... @@ -403,6 +411,8 @@ intermediate model. The intermediate model is associated with the that will act as an intermediary. For our musician example, the code would look something like this:: + from django.db import models + class Person(models.Model): name = models.CharField(max_length=128) @@ -583,6 +593,7 @@ It's perfectly OK to relate a model to one from another app. To do this, import the related model at the top of the file where your model is defined. Then, just refer to the other model class wherever needed. For example:: + from django.db import models from geography.models import ZipCode class Restaurant(models.Model): @@ -630,6 +641,8 @@ Meta options Give your model metadata by using an inner ``class Meta``, like so:: + from django.db import models + class Ox(models.Model): horn_length = models.IntegerField() @@ -660,6 +673,8 @@ model. For example, this model has a few custom methods:: + from django.db import models + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -729,6 +744,8 @@ A classic use-case for overriding the built-in methods is if you want something to happen whenever you save an object. For example (see :meth:`~Model.save` for documentation of the parameters it accepts):: + from django.db import models + class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() @@ -740,6 +757,8 @@ to happen whenever you save an object. For example (see You can also prevent saving:: + from django.db import models + class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() @@ -826,6 +845,8 @@ the child (and Django will raise an exception). An example:: + from django.db import models + class CommonInfo(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() @@ -854,14 +875,16 @@ attribute. If a child class does not declare its own :ref:`Meta ` class, it will inherit the parent's :ref:`Meta `. If the child wants to extend the parent's :ref:`Meta ` class, it can subclass it. For example:: + from django.db import models + class CommonInfo(models.Model): - ... + # ... class Meta: abstract = True ordering = ['name'] class Student(CommonInfo): - ... + # ... class Meta(CommonInfo.Meta): db_table = 'student_info' @@ -901,6 +924,8 @@ abstract base class (only), part of the name should contain For example, given an app ``common/models.py``:: + from django.db import models + class Base(models.Model): m2m = models.ManyToManyField(OtherModel, related_name="%(app_label)s_%(class)s_related") @@ -949,6 +974,8 @@ relationship introduces links between the child model and each of its parents (via an automatically-created :class:`~django.db.models.OneToOneField`). For example:: + from django.db import models + class Place(models.Model): name = models.CharField(max_length=50) address = models.CharField(max_length=80) @@ -998,7 +1025,7 @@ If the parent has an ordering and you don't want the child to have any natural ordering, you can explicitly disable it:: class ChildModel(ParentModel): - ... + # ... class Meta: # Remove parent's ordering effect ordering = [] @@ -1061,15 +1088,21 @@ Proxy models are declared like normal models. You tell Django that it's a proxy model by setting the :attr:`~django.db.models.Options.proxy` attribute of the ``Meta`` class to ``True``. -For example, suppose you want to add a method to the ``Person`` model described -above. You can do it like this:: +For example, suppose you want to add a method to the ``Person`` model. You can do it like this:: + + from django.db import models + + class Person(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) class MyPerson(Person): class Meta: proxy = True def do_something(self): - ... + # ... + pass The ``MyPerson`` class operates on the same database table as its parent ``Person`` class. In particular, any new instances of ``Person`` will also be @@ -1125,8 +1158,11 @@ classes will still be available. Continuing our example from above, you could change the default manager used when you query the ``Person`` model like this:: + from django.db import models + class NewManager(models.Manager): - ... + # ... + pass class MyPerson(Person): objects = NewManager() diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 2553eac27a..a1cb5c79c5 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -17,6 +17,8 @@ models, which comprise a Weblog application: .. code-block:: python + from django.db import models + class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() -- cgit v1.3 From cd72c55d8603751af40a55d2d18f264827fa0744 Mon Sep 17 00:00:00 2001 From: Silvan Spross Date: Sat, 18 May 2013 14:00:52 +0200 Subject: Add missing imports and models to the examples in the view layer documentation --- docs/ref/template-response.txt | 3 +++ docs/topics/class-based-views/generic-display.txt | 6 +++-- docs/topics/class-based-views/generic-editing.txt | 1 + docs/topics/class-based-views/mixins.txt | 8 ++++++ docs/topics/files.txt | 2 ++ docs/topics/http/file-uploads.txt | 2 ++ docs/topics/http/urls.txt | 31 +++++++++++++++++++++++ docs/topics/http/views.txt | 6 +++++ 8 files changed, 57 insertions(+), 2 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index cdefe2fae8..4f34d150ed 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -215,6 +215,7 @@ re-rendered, you can re-evaluate the rendered content, and assign the content of the response manually:: # Set up a rendered TemplateResponse + >>> from django.template.response import TemplateResponse >>> t = TemplateResponse(request, 'original.html', {}) >>> t.render() >>> print(t.content) @@ -256,6 +257,8 @@ To define a post-render callback, just define a function that takes a single argument -- response -- and register that function with the template response:: + from django.template.response import TemplateResponse + def my_render_callback(response): # Do content-sensitive processing do_post_processing() diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index 64b998770f..7ffa471e79 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -248,7 +248,7 @@ specify the objects that the view will operate upon -- you can also specify the list of objects using the ``queryset`` argument:: from django.views.generic import DetailView - from books.models import Publisher, Book + from books.models import Publisher class PublisherDetail(DetailView): @@ -326,6 +326,7 @@ various useful things are stored on ``self``; as well as the request Here, we have a URLconf with a single captured group:: # urls.py + from django.conf.urls import patterns from books.views import PublisherBookList urlpatterns = patterns('', @@ -375,6 +376,7 @@ Imagine we had a ``last_accessed`` field on our ``Author`` object that we were using to keep track of the last time anybody looked at that author:: # models.py + from django.db import models class Author(models.Model): salutation = models.CharField(max_length=10) @@ -390,6 +392,7 @@ updated. First, we'd need to add an author detail bit in the URLconf to point to a custom view:: + from django.conf.urls import patterns, url from books.views import AuthorDetailView urlpatterns = patterns('', @@ -401,7 +404,6 @@ Then we'd write our new view -- ``get_object`` is the method that retrieves the object -- so we simply override it and wrap the call:: from django.views.generic import DetailView - from django.shortcuts import get_object_or_404 from django.utils import timezone from books.models import Author diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt index 86c5280159..7c4e02cc4e 100644 --- a/docs/topics/class-based-views/generic-editing.txt +++ b/docs/topics/class-based-views/generic-editing.txt @@ -222,6 +222,7 @@ works for AJAX requests as well as 'normal' form POSTs:: from django.http import HttpResponse from django.views.generic.edit import CreateView + from myapp.models import Author class AjaxableResponseMixin(object): """ diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index 9550d2fb86..980e571c85 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -258,6 +258,7 @@ mixin. We can hook this into our URLs easily enough:: # urls.py + from django.conf.urls import patterns, url from books.views import RecordInterest urlpatterns = patterns('', @@ -440,6 +441,7 @@ Our new ``AuthorDetail`` looks like this:: from django.core.urlresolvers import reverse from django.views.generic import DetailView from django.views.generic.edit import FormMixin + from books.models import Author class AuthorInterestForm(forms.Form): message = forms.CharField() @@ -546,6 +548,8 @@ template as ``AuthorDisplay`` is using on ``GET``. .. code-block:: python + from django.core.urlresolvers import reverse + from django.http import HttpResponseForbidden from django.views.generic import FormView from django.views.generic.detail import SingleObjectMixin @@ -657,6 +661,8 @@ own version of :class:`~django.views.generic.detail.DetailView` by mixing :class:`~django.views.generic.detail.DetailView` before template rendering behavior has been mixed in):: + from django.views.generic.detail import BaseDetailView + class JSONDetailView(JSONResponseMixin, BaseDetailView): pass @@ -675,6 +681,8 @@ and override the implementation of to defer to the appropriate subclass depending on the type of response that the user requested:: + from django.views.generic.detail import SingleObjectTemplateResponseMixin + class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView): def render_to_response(self, context): # Look for a 'format=json' GET argument diff --git a/docs/topics/files.txt b/docs/topics/files.txt index fb3cdd4af9..492e6a20b5 100644 --- a/docs/topics/files.txt +++ b/docs/topics/files.txt @@ -27,6 +27,8 @@ to deal with that file. Consider the following model, using an :class:`~django.db.models.ImageField` to store a photo:: + from django.db import models + class Car(models.Model): name = models.CharField(max_length=255) price = models.DecimalField(max_digits=5, decimal_places=2) diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index 80bd5f3c44..54d748d961 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -15,6 +15,7 @@ Basic file uploads Consider a simple form containing a :class:`~django.forms.FileField`:: + # In forms.py... from django import forms class UploadFileForm(forms.Form): @@ -39,6 +40,7 @@ something like:: from django.http import HttpResponseRedirect from django.shortcuts import render_to_response + from .forms import UploadFileForm # Imaginary function to handle an uploaded file. from somewhere import handle_uploaded_file diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 9a96199dba..8a3f240307 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -123,6 +123,8 @@ is ``(?Ppattern)``, where ``name`` is the name of the group and Here's the above example URLconf, rewritten to use named groups:: + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^articles/2003/$', 'news.views.special_case_2003'), url(r'^articles/(?P\d{4})/$', 'news.views.year_archive'), @@ -192,6 +194,8 @@ A convenient trick is to specify default parameters for your views' arguments. Here's an example URLconf and view:: # URLconf + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^blog/$', 'blog.views.page'), url(r'^blog/page(?P\d+)/$', 'blog.views.page'), @@ -370,11 +374,15 @@ An included URLconf receives any captured parameters from parent URLconfs, so the following example is valid:: # In settings/urls/main.py + from django.conf.urls import include, patterns, url + urlpatterns = patterns('', url(r'^(?P\w+)/blog/', include('foo.urls.blog')), ) # In foo/urls/blog.py + from django.conf.urls import patterns, url + urlpatterns = patterns('foo.views', url(r'^$', 'blog.index'), url(r'^archive/$', 'blog.archive'), @@ -397,6 +405,8 @@ function. For example:: + from django.conf.urls import patterns, url + urlpatterns = patterns('blog.views', url(r'^blog/(?P\d{4})/$', 'year_archive', {'foo': 'bar'}), ) @@ -427,11 +437,15 @@ For example, these two URLconf sets are functionally identical: Set one:: # main.py + from django.conf.urls import include, patterns, url + urlpatterns = patterns('', url(r'^blog/', include('inner'), {'blogid': 3}), ) # inner.py + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^archive/$', 'mysite.views.archive'), url(r'^about/$', 'mysite.views.about'), @@ -440,11 +454,15 @@ Set one:: Set two:: # main.py + from django.conf.urls import include, patterns, url + urlpatterns = patterns('', url(r'^blog/', include('inner')), ) # inner.py + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^archive/$', 'mysite.views.archive', {'blogid': 3}), url(r'^about/$', 'mysite.views.about', {'blogid': 3}), @@ -464,6 +482,8 @@ supported -- you can pass any callable object as the view. For example, given this URLconf in "string" notation:: + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^archive/$', 'mysite.views.archive'), url(r'^about/$', 'mysite.views.about'), @@ -473,6 +493,7 @@ For example, given this URLconf in "string" notation:: You can accomplish the same thing by passing objects rather than strings. Just be sure to import the objects:: + from django.conf.urls import patterns, url from mysite.views import archive, about, contact urlpatterns = patterns('', @@ -485,6 +506,7 @@ The following example is functionally identical. It's just a bit more compact because it imports the module that contains the views, rather than importing each view individually:: + from django.conf.urls import patterns, url from mysite import views urlpatterns = patterns('', @@ -501,6 +523,7 @@ the view prefix (as explained in "The view prefix" above) will have no effect. Note that :doc:`class based views` must be imported:: + from django.conf.urls import patterns, url from mysite.views import ClassBasedView urlpatterns = patterns('', @@ -612,6 +635,9 @@ It's fairly common to use the same view function in multiple URL patterns in your URLconf. For example, these two URL patterns both point to the ``archive`` view:: + from django.conf.urls import patterns, url + from mysite.views import archive + urlpatterns = patterns('', url(r'^archive/(\d{4})/$', archive), url(r'^archive-summary/(\d{4})/$', archive, {'summary': True}), @@ -630,6 +656,9 @@ matching. Here's the above example, rewritten to use named URL patterns:: + from django.conf.urls import patterns, url + from mysite.views import archive + urlpatterns = patterns('', url(r'^archive/(\d{4})/$', archive, name="full-archive"), url(r'^archive-summary/(\d{4})/$', archive, {'summary': True}, name="arch-summary"), @@ -803,6 +832,8 @@ However, you can also ``include()`` a 3-tuple containing:: For example:: + from django.conf.urls import include, patterns, url + help_patterns = patterns('', url(r'^basic/$', 'apps.help.views.views.basic'), url(r'^advanced/$', 'apps.help.views.views.advanced'), diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index f73ec4f5be..2ccedec2f7 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -70,6 +70,8 @@ documentation. Just return an instance of one of those subclasses instead of a normal :class:`~django.http.HttpResponse` in order to signify an error. For example:: + from django.http import HttpResponse, HttpResponseNotFound + def my_view(request): # ... if foo: @@ -83,6 +85,8 @@ the :class:`~django.http.HttpResponse` documentation, you can also pass the HTTP status code into the constructor for :class:`~django.http.HttpResponse` to create a return class for any status code you like. For example:: + from django.http import HttpResponse + def my_view(request): # ... @@ -110,6 +114,8 @@ standard error page for your application, along with an HTTP error code 404. Example usage:: from django.http import Http404 + from django.shortcuts import render_to_response + from polls.models import Poll def detail(request, poll_id): try: -- cgit v1.3 From 0a50311063c416ec4d39f518e8d8110dd7eddbdf Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sat, 18 May 2013 19:04:34 -0300 Subject: Fixed #20004 -- Moved non DB-related assertions to SimpleTestCase. Thanks zalew for the suggestion and work on a patch. Also updated, tweaked and fixed testing documentation. --- django/test/testcases.py | 515 +++++++++++++++++++------------------- docs/intro/tutorial05.txt | 4 +- docs/ref/contrib/contenttypes.txt | 2 +- docs/releases/1.3-alpha-1.txt | 2 +- docs/releases/1.3.txt | 2 +- docs/releases/1.4.txt | 8 +- docs/releases/1.6.txt | 7 +- docs/topics/python3.txt | 4 +- docs/topics/testing/overview.txt | 235 ++++++++++------- 9 files changed, 417 insertions(+), 362 deletions(-) (limited to 'docs/ref') diff --git a/django/test/testcases.py b/django/test/testcases.py index 6f8cbabd86..311b50cfb7 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -231,6 +231,10 @@ class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext): class SimpleTestCase(ut2.TestCase): + # The class we'll use for the test client self.client. + # Can be overridden in derived classes. + client_class = Client + _warn_txt = ("save_warnings_state/restore_warnings_state " "django.test.*TestCase methods are deprecated. Use Python's " "warnings.catch_warnings context manager instead.") @@ -264,10 +268,31 @@ class SimpleTestCase(ut2.TestCase): return def _pre_setup(self): - pass + """Performs any pre-test setup. This includes: + + * If the Test Case class has a 'urls' member, replace the + ROOT_URLCONF with it. + * Clearing the mail test outbox. + """ + self.client = self.client_class() + self._urlconf_setup() + mail.outbox = [] + + def _urlconf_setup(self): + set_urlconf(None) + if hasattr(self, 'urls'): + self._old_root_urlconf = settings.ROOT_URLCONF + settings.ROOT_URLCONF = self.urls + clear_url_caches() def _post_teardown(self): - pass + self._urlconf_teardown() + + def _urlconf_teardown(self): + set_urlconf(None) + if hasattr(self, '_old_root_urlconf'): + settings.ROOT_URLCONF = self._old_root_urlconf + clear_url_caches() def save_warnings_state(self): """ @@ -291,258 +316,6 @@ class SimpleTestCase(ut2.TestCase): """ return override_settings(**kwargs) - def assertRaisesMessage(self, expected_exception, expected_message, - callable_obj=None, *args, **kwargs): - """ - Asserts that the message in a raised exception matches the passed - value. - - Args: - expected_exception: Exception class expected to be raised. - expected_message: expected error message string value. - callable_obj: Function to be called. - args: Extra args. - kwargs: Extra kwargs. - """ - return six.assertRaisesRegex(self, expected_exception, - re.escape(expected_message), callable_obj, *args, **kwargs) - - def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, - field_kwargs=None, empty_value=''): - """ - Asserts that a form field behaves correctly with various inputs. - - Args: - fieldclass: the class of the field to be tested. - valid: a dictionary mapping valid inputs to their expected - cleaned values. - invalid: a dictionary mapping invalid inputs to one or more - raised error messages. - field_args: the args passed to instantiate the field - field_kwargs: the kwargs passed to instantiate the field - empty_value: the expected clean output for inputs in empty_values - - """ - if field_args is None: - field_args = [] - if field_kwargs is None: - field_kwargs = {} - required = fieldclass(*field_args, **field_kwargs) - optional = fieldclass(*field_args, - **dict(field_kwargs, required=False)) - # test valid inputs - for input, output in valid.items(): - self.assertEqual(required.clean(input), output) - self.assertEqual(optional.clean(input), output) - # test invalid inputs - for input, errors in invalid.items(): - with self.assertRaises(ValidationError) as context_manager: - required.clean(input) - self.assertEqual(context_manager.exception.messages, errors) - - with self.assertRaises(ValidationError) as context_manager: - optional.clean(input) - self.assertEqual(context_manager.exception.messages, errors) - # test required inputs - error_required = [force_text(required.error_messages['required'])] - for e in required.empty_values: - with self.assertRaises(ValidationError) as context_manager: - required.clean(e) - self.assertEqual(context_manager.exception.messages, - error_required) - self.assertEqual(optional.clean(e), empty_value) - # test that max_length and min_length are always accepted - if issubclass(fieldclass, CharField): - field_kwargs.update({'min_length':2, 'max_length':20}) - self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs), - fieldclass)) - - def assertHTMLEqual(self, html1, html2, msg=None): - """ - Asserts that two HTML snippets are semantically the same. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid HTML. - """ - dom1 = assert_and_parse_html(self, html1, msg, - 'First argument is not valid HTML:') - dom2 = assert_and_parse_html(self, html2, msg, - 'Second argument is not valid HTML:') - - if dom1 != dom2: - standardMsg = '%s != %s' % ( - safe_repr(dom1, True), safe_repr(dom2, True)) - diff = ('\n' + '\n'.join(difflib.ndiff( - six.text_type(dom1).splitlines(), - six.text_type(dom2).splitlines()))) - standardMsg = self._truncateMessage(standardMsg, diff) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertHTMLNotEqual(self, html1, html2, msg=None): - """Asserts that two HTML snippets are not semantically equivalent.""" - dom1 = assert_and_parse_html(self, html1, msg, - 'First argument is not valid HTML:') - dom2 = assert_and_parse_html(self, html2, msg, - 'Second argument is not valid HTML:') - - if dom1 == dom2: - standardMsg = '%s == %s' % ( - safe_repr(dom1, True), safe_repr(dom2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertInHTML(self, needle, haystack, count = None, msg_prefix=''): - needle = assert_and_parse_html(self, needle, None, - 'First argument is not valid HTML:') - haystack = assert_and_parse_html(self, haystack, None, - 'Second argument is not valid HTML:') - real_count = haystack.count(needle) - if count is not None: - self.assertEqual(real_count, count, - msg_prefix + "Found %d instances of '%s' in response" - " (expected %d)" % (real_count, needle, count)) - else: - self.assertTrue(real_count != 0, - msg_prefix + "Couldn't find '%s' in response" % needle) - - def assertJSONEqual(self, raw, expected_data, msg=None): - try: - data = json.loads(raw) - except ValueError: - self.fail("First argument is not valid JSON: %r" % raw) - if isinstance(expected_data, six.string_types): - try: - expected_data = json.loads(expected_data) - except ValueError: - self.fail("Second argument is not valid JSON: %r" % expected_data) - self.assertEqual(data, expected_data, msg=msg) - - def assertXMLEqual(self, xml1, xml2, msg=None): - """ - Asserts that two XML snippets are semantically the same. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid XML. - """ - try: - result = compare_xml(xml1, xml2) - except Exception as e: - standardMsg = 'First or second argument is not valid XML\n%s' % e - self.fail(self._formatMessage(msg, standardMsg)) - else: - if not result: - standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertXMLNotEqual(self, xml1, xml2, msg=None): - """ - Asserts that two XML snippets are not semantically equivalent. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid XML. - """ - try: - result = compare_xml(xml1, xml2) - except Exception as e: - standardMsg = 'First or second argument is not valid XML\n%s' % e - self.fail(self._formatMessage(msg, standardMsg)) - else: - if result: - standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - -class TransactionTestCase(SimpleTestCase): - - # The class we'll use for the test client self.client. - # Can be overridden in derived classes. - client_class = Client - - # Subclasses can ask for resetting of auto increment sequence before each - # test case - reset_sequences = False - - def _pre_setup(self): - """Performs any pre-test setup. This includes: - - * Flushing the database. - * If the Test Case class has a 'fixtures' member, installing the - named fixtures. - * If the Test Case class has a 'urls' member, replace the - ROOT_URLCONF with it. - * Clearing the mail test outbox. - """ - self.client = self.client_class() - self._fixture_setup() - self._urlconf_setup() - mail.outbox = [] - - def _databases_names(self, include_mirrors=True): - # If the test case has a multi_db=True flag, act on all databases, - # including mirrors or not. Otherwise, just on the default DB. - if getattr(self, 'multi_db', False): - return [alias for alias in connections - if include_mirrors or not connections[alias].settings_dict['TEST_MIRROR']] - else: - return [DEFAULT_DB_ALIAS] - - def _reset_sequences(self, db_name): - conn = connections[db_name] - if conn.features.supports_sequence_reset: - sql_list = \ - conn.ops.sequence_reset_by_name_sql(no_style(), - conn.introspection.sequence_list()) - if sql_list: - with transaction.commit_on_success_unless_managed(using=db_name): - cursor = conn.cursor() - for sql in sql_list: - cursor.execute(sql) - - def _fixture_setup(self): - for db_name in self._databases_names(include_mirrors=False): - # Reset sequences - if self.reset_sequences: - self._reset_sequences(db_name) - - if hasattr(self, 'fixtures'): - # We have to use this slightly awkward syntax due to the fact - # that we're using *args and **kwargs together. - call_command('loaddata', *self.fixtures, - **{'verbosity': 0, 'database': db_name, 'skip_validation': True}) - - def _urlconf_setup(self): - set_urlconf(None) - if hasattr(self, 'urls'): - self._old_root_urlconf = settings.ROOT_URLCONF - settings.ROOT_URLCONF = self.urls - clear_url_caches() - - def _post_teardown(self): - """ Performs any post-test things. This includes: - - * Putting back the original ROOT_URLCONF if it was changed. - * Force closing the connection, so that the next test gets - a clean cursor. - """ - self._fixture_teardown() - self._urlconf_teardown() - # Some DB cursors include SQL statements as part of cursor - # creation. If you have a test that does rollback, the effect - # of these statements is lost, which can effect the operation - # of tests (e.g., losing a timezone setting causing objects to - # be created with the wrong time). - # To make sure this doesn't happen, get a clean connection at the - # start of every test. - for conn in connections.all(): - conn.close() - - def _fixture_teardown(self): - for db in self._databases_names(include_mirrors=False): - call_command('flush', verbosity=0, interactive=False, database=db, - skip_validation=True, reset_sequences=False) - - def _urlconf_teardown(self): - set_urlconf(None) - if hasattr(self, '_old_root_urlconf'): - settings.ROOT_URLCONF = self._old_root_urlconf - clear_url_caches() - def assertRedirects(self, response, expected_url, status_code=302, target_status_code=200, host=None, msg_prefix=''): """Asserts that a response redirected to a specific URL, and that the @@ -787,6 +560,236 @@ class TransactionTestCase(SimpleTestCase): msg_prefix + "Template '%s' was used unexpectedly in rendering" " the response" % template_name) + def assertRaisesMessage(self, expected_exception, expected_message, + callable_obj=None, *args, **kwargs): + """ + Asserts that the message in a raised exception matches the passed + value. + + Args: + expected_exception: Exception class expected to be raised. + expected_message: expected error message string value. + callable_obj: Function to be called. + args: Extra args. + kwargs: Extra kwargs. + """ + return six.assertRaisesRegex(self, expected_exception, + re.escape(expected_message), callable_obj, *args, **kwargs) + + def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, + field_kwargs=None, empty_value=''): + """ + Asserts that a form field behaves correctly with various inputs. + + Args: + fieldclass: the class of the field to be tested. + valid: a dictionary mapping valid inputs to their expected + cleaned values. + invalid: a dictionary mapping invalid inputs to one or more + raised error messages. + field_args: the args passed to instantiate the field + field_kwargs: the kwargs passed to instantiate the field + empty_value: the expected clean output for inputs in empty_values + + """ + if field_args is None: + field_args = [] + if field_kwargs is None: + field_kwargs = {} + required = fieldclass(*field_args, **field_kwargs) + optional = fieldclass(*field_args, + **dict(field_kwargs, required=False)) + # test valid inputs + for input, output in valid.items(): + self.assertEqual(required.clean(input), output) + self.assertEqual(optional.clean(input), output) + # test invalid inputs + for input, errors in invalid.items(): + with self.assertRaises(ValidationError) as context_manager: + required.clean(input) + self.assertEqual(context_manager.exception.messages, errors) + + with self.assertRaises(ValidationError) as context_manager: + optional.clean(input) + self.assertEqual(context_manager.exception.messages, errors) + # test required inputs + error_required = [force_text(required.error_messages['required'])] + for e in required.empty_values: + with self.assertRaises(ValidationError) as context_manager: + required.clean(e) + self.assertEqual(context_manager.exception.messages, + error_required) + self.assertEqual(optional.clean(e), empty_value) + # test that max_length and min_length are always accepted + if issubclass(fieldclass, CharField): + field_kwargs.update({'min_length':2, 'max_length':20}) + self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs), + fieldclass)) + + def assertHTMLEqual(self, html1, html2, msg=None): + """ + Asserts that two HTML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid HTML. + """ + dom1 = assert_and_parse_html(self, html1, msg, + 'First argument is not valid HTML:') + dom2 = assert_and_parse_html(self, html2, msg, + 'Second argument is not valid HTML:') + + if dom1 != dom2: + standardMsg = '%s != %s' % ( + safe_repr(dom1, True), safe_repr(dom2, True)) + diff = ('\n' + '\n'.join(difflib.ndiff( + six.text_type(dom1).splitlines(), + six.text_type(dom2).splitlines()))) + standardMsg = self._truncateMessage(standardMsg, diff) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertHTMLNotEqual(self, html1, html2, msg=None): + """Asserts that two HTML snippets are not semantically equivalent.""" + dom1 = assert_and_parse_html(self, html1, msg, + 'First argument is not valid HTML:') + dom2 = assert_and_parse_html(self, html2, msg, + 'Second argument is not valid HTML:') + + if dom1 == dom2: + standardMsg = '%s == %s' % ( + safe_repr(dom1, True), safe_repr(dom2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertInHTML(self, needle, haystack, count=None, msg_prefix=''): + needle = assert_and_parse_html(self, needle, None, + 'First argument is not valid HTML:') + haystack = assert_and_parse_html(self, haystack, None, + 'Second argument is not valid HTML:') + real_count = haystack.count(needle) + if count is not None: + self.assertEqual(real_count, count, + msg_prefix + "Found %d instances of '%s' in response" + " (expected %d)" % (real_count, needle, count)) + else: + self.assertTrue(real_count != 0, + msg_prefix + "Couldn't find '%s' in response" % needle) + + def assertJSONEqual(self, raw, expected_data, msg=None): + try: + data = json.loads(raw) + except ValueError: + self.fail("First argument is not valid JSON: %r" % raw) + if isinstance(expected_data, six.string_types): + try: + expected_data = json.loads(expected_data) + except ValueError: + self.fail("Second argument is not valid JSON: %r" % expected_data) + self.assertEqual(data, expected_data, msg=msg) + + def assertXMLEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if not result: + standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertXMLNotEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are not semantically equivalent. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if result: + standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + +class TransactionTestCase(SimpleTestCase): + + # Subclasses can ask for resetting of auto increment sequence before each + # test case + reset_sequences = False + + def _pre_setup(self): + """Performs any pre-test setup. This includes: + + * Flushing the database. + * If the Test Case class has a 'fixtures' member, installing the + named fixtures. + """ + super(TransactionTestCase, self)._pre_setup() + self._fixture_setup() + + def _databases_names(self, include_mirrors=True): + # If the test case has a multi_db=True flag, act on all databases, + # including mirrors or not. Otherwise, just on the default DB. + if getattr(self, 'multi_db', False): + return [alias for alias in connections + if include_mirrors or not connections[alias].settings_dict['TEST_MIRROR']] + else: + return [DEFAULT_DB_ALIAS] + + def _reset_sequences(self, db_name): + conn = connections[db_name] + if conn.features.supports_sequence_reset: + sql_list = \ + conn.ops.sequence_reset_by_name_sql(no_style(), + conn.introspection.sequence_list()) + if sql_list: + with transaction.commit_on_success_unless_managed(using=db_name): + cursor = conn.cursor() + for sql in sql_list: + cursor.execute(sql) + + def _fixture_setup(self): + for db_name in self._databases_names(include_mirrors=False): + # Reset sequences + if self.reset_sequences: + self._reset_sequences(db_name) + + if hasattr(self, 'fixtures'): + # We have to use this slightly awkward syntax due to the fact + # that we're using *args and **kwargs together. + call_command('loaddata', *self.fixtures, + **{'verbosity': 0, 'database': db_name, 'skip_validation': True}) + + def _post_teardown(self): + """Performs any post-test things. This includes: + + * Putting back the original ROOT_URLCONF if it was changed. + * Force closing the connection, so that the next test gets + a clean cursor. + """ + self._fixture_teardown() + super(TransactionTestCase, self)._post_teardown() + # Some DB cursors include SQL statements as part of cursor + # creation. If you have a test that does rollback, the effect + # of these statements is lost, which can effect the operation + # of tests (e.g., losing a timezone setting causing objects to + # be created with the wrong time). + # To make sure this doesn't happen, get a clean connection at the + # start of every test. + for conn in connections.all(): + conn.close() + + def _fixture_teardown(self): + for db_name in self._databases_names(include_mirrors=False): + call_command('flush', verbosity=0, interactive=False, database=db_name, + skip_validation=True, reset_sequences=False) + def assertQuerysetEqual(self, qs, values, transform=repr, ordered=True): items = six.moves.map(transform, qs) if not ordered: @@ -841,14 +844,14 @@ class TestCase(TransactionTestCase): # Remove this when the legacy transaction management goes away. disable_transaction_methods() - for db in self._databases_names(include_mirrors=False): + for db_name in self._databases_names(include_mirrors=False): if hasattr(self, 'fixtures'): try: call_command('loaddata', *self.fixtures, **{ 'verbosity': 0, 'commit': False, - 'database': db, + 'database': db_name, 'skip_validation': True, }) except Exception: diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index a276763d67..39c3785f7c 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -503,8 +503,8 @@ of the process of creating polls. message: "No polls are available." and verifies the ``latest_poll_list`` is empty. Note that the :class:`django.test.TestCase` class provides some additional assertion methods. In these examples, we use -:meth:`~django.test.TestCase.assertContains()` and -:meth:`~django.test.TestCase.assertQuerysetEqual()`. +:meth:`~django.test.SimpleTestCase.assertContains()` and +:meth:`~django.test.TransactionTestCase.assertQuerysetEqual()`. In ``test_index_view_with_a_past_poll``, we create a poll and verify that it appears in the list. diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 4fa119bc70..1bb0802442 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -329,7 +329,7 @@ model: .. admonition:: Serializing references to ``ContentType`` objects If you're serializing data (for example, when generating - :class:`~django.test.TestCase.fixtures`) from a model that implements + :class:`~django.test.TransactionTestCase.fixtures`) from a model that implements generic relations, you should probably be using a natural key to uniquely identify related :class:`~django.contrib.contenttypes.models.ContentType` objects. See :ref:`natural keys` and diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index 42947d9a44..634e6afaf2 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -154,7 +154,7 @@ requests. These include: requests in tests. * A new test assertion -- - :meth:`~django.test.TestCase.assertNumQueries` -- making it + :meth:`~django.test.TransactionTestCase.assertNumQueries` -- making it easier to test the database activity associated with a view. diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 89cece941b..45ebb2f1fe 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -299,7 +299,7 @@ requests. These include: in tests. * A new test assertion -- - :meth:`~django.test.TestCase.assertNumQueries` -- making it + :meth:`~django.test.TransactionTestCase.assertNumQueries` -- making it easier to test the database activity associated with a view. * Support for lookups spanning relations in admin's diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 83a5f54fc7..a013665ad3 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -541,8 +541,8 @@ compare HTML directly with the new :meth:`~django.test.SimpleTestCase.assertHTMLEqual` and :meth:`~django.test.SimpleTestCase.assertHTMLNotEqual` assertions, or use the ``html=True`` flag with -:meth:`~django.test.TestCase.assertContains` and -:meth:`~django.test.TestCase.assertNotContains` to test whether the +:meth:`~django.test.SimpleTestCase.assertContains` and +:meth:`~django.test.SimpleTestCase.assertNotContains` to test whether the client's response contains a given HTML fragment. See the :ref:`assertions documentation ` for more. @@ -1093,8 +1093,8 @@ wild, because they would confuse browsers too. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's now possible to check whether a template was used within a block of -code with :meth:`~django.test.TestCase.assertTemplateUsed` and -:meth:`~django.test.TestCase.assertTemplateNotUsed`. And they +code with :meth:`~django.test.SimpleTestCase.assertTemplateUsed` and +:meth:`~django.test.SimpleTestCase.assertTemplateNotUsed`. And they can be used as a context manager:: with self.assertTemplateUsed('index.html'): diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index f8e1fd6339..0eab8540b0 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -271,9 +271,10 @@ The changes in transaction management may result in additional statements to create, release or rollback savepoints. This is more likely to happen with SQLite, since it didn't support savepoints until this release. -If tests using :meth:`~django.test.TestCase.assertNumQueries` fail because of -a higher number of queries than expected, check that the extra queries are -related to savepoints, and adjust the expected number of queries accordingly. +If tests using :meth:`~django.test.TransactionTestCase.assertNumQueries` fail +because of a higher number of queries than expected, check that the extra +queries are related to savepoints, and adjust the expected number of queries +accordingly. Autocommit option for PostgreSQL ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index 22e609c75c..9a0438e9e5 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -201,8 +201,8 @@ According to :pep:`3333`: Specifically, :attr:`HttpResponse.content ` contains ``bytes``, which may become an issue if you compare it with a ``str`` in your tests. The preferred solution is to rely on -:meth:`~django.test.TestCase.assertContains` and -:meth:`~django.test.TestCase.assertNotContains`. These methods accept a +:meth:`~django.test.SimpleTestCase.assertContains` and +:meth:`~django.test.SimpleTestCase.assertNotContains`. These methods accept a response and a unicode string as arguments. Coding guidelines diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index fc2b393898..d543099ae6 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -21,17 +21,16 @@ module defines tests using a class-based approach. .. admonition:: unittest2 - Python 2.7 introduced some major changes to the unittest library, + Python 2.7 introduced some major changes to the ``unittest`` library, adding some extremely useful features. To ensure that every Django project can benefit from these new features, Django ships with a - copy of unittest2_, a copy of the Python 2.7 unittest library, - backported for Python 2.6 compatibility. + copy of unittest2_, a copy of Python 2.7's ``unittest``, backported for + Python 2.6 compatibility. To access this library, Django provides the ``django.utils.unittest`` module alias. If you are using Python - 2.7, or you have installed unittest2 locally, Django will map the - alias to the installed version of the unittest library. Otherwise, - Django will use its own bundled version of unittest2. + 2.7, or you have installed ``unittest2`` locally, Django will map the alias + to it. Otherwise, Django will use its own bundled version of ``unittest2``. To use this alias, simply use:: @@ -41,8 +40,8 @@ module defines tests using a class-based approach. import unittest - If you want to continue to use the base unittest library, you can -- - you just won't get any of the nice new unittest2 features. + If you want to continue to use the legacy ``unittest`` library, you can -- + you just won't get any of the nice new ``unittest2`` features. .. _unittest2: http://pypi.python.org/pypi/unittest2 @@ -858,24 +857,46 @@ SimpleTestCase .. class:: SimpleTestCase() -A very thin subclass of :class:`unittest.TestCase`, it extends it with some -basic functionality like: +A thin subclass of :class:`unittest.TestCase`, it extends it with some basic +functionality like: * Saving and restoring the Python warning machinery state. -* Checking that a callable :meth:`raises a certain exception `. -* :meth:`Testing form field rendering `. -* Testing server :ref:`HTML responses for the presence/lack of a given fragment `. -* The ability to run tests with :ref:`modified settings ` +* Some useful assertions like: + + * Checking that a callable :meth:`raises a certain exception + `. + * Testing form field :meth:`rendering and error treatment + `. + * Testing :meth:`HTML responses for the presence/lack of a given fragment + `. + * Verifying that a template :meth:`has/hasn't been used to generate a given + response content `. + * Verifying a HTTP :meth:`redirect ` is + performed by the app. + * Robustly testing two :meth:`HTML fragments ` + for equality/inequality or :meth:`containment `. + * Robustly testing two :meth:`XML fragments ` + for equality/inequality. + * Robustly testing two :meth:`JSON fragments ` + for equality. + +* The ability to run tests with :ref:`modified settings `. +* Using the :attr:`~SimpleTestCase.client` :class:`~django.test.client.Client`. +* Custom test-time :attr:`URL maps `. + +.. versionchanged:: 1.6 + + The latter two features were moved from ``TransactionTestCase`` to + ``SimpleTestCase`` in Django 1.6. If you need any of the other more complex and heavyweight Django-specific features like: -* Using the :attr:`~TestCase.client` :class:`~django.test.client.Client`. * Testing or using the ORM. -* Database :attr:`~TestCase.fixtures`. -* Custom test-time :attr:`URL maps `. +* Database :attr:`~TransactionTestCase.fixtures`. * Test :ref:`skipping based on database backend features `. -* The remaining specialized :ref:`assert* ` methods. +* The remaining specialized :meth:`assert* + ` methods. then you should use :class:`~django.test.TransactionTestCase` or :class:`~django.test.TestCase` instead. @@ -1137,9 +1158,9 @@ Test cases features Default test client ~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.client +.. attribute:: SimpleTestCase.client -Every test case in a ``django.test.TestCase`` instance has access to an +Every test case in a ``django.test.*TestCase`` instance has access to an instance of a Django test client. This client can be accessed as ``self.client``. This client is recreated for each test, so you don't have to worry about state (such as cookies) carrying over from one test to another. @@ -1176,10 +1197,10 @@ This means, instead of instantiating a ``Client`` in each test:: Customizing the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.client_class +.. attribute:: SimpleTestCase.client_class If you want to use a different ``Client`` class (for example, a subclass -with customized behavior), use the :attr:`~TestCase.client_class` class +with customized behavior), use the :attr:`~SimpleTestCase.client_class` class attribute:: from django.test import TestCase @@ -1200,11 +1221,12 @@ attribute:: Fixture loading ~~~~~~~~~~~~~~~ -.. attribute:: TestCase.fixtures +.. attribute:: TransactionTestCase.fixtures A test case for a database-backed Web site isn't much use if there isn't any data in the database. To make it easy to put test data into the database, -Django's custom ``TestCase`` class provides a way of loading **fixtures**. +Django's custom ``TransactionTestCase`` class provides a way of loading +**fixtures**. A fixture is a collection of data that Django knows how to import into a database. For example, if your site has user accounts, you might set up a @@ -1273,7 +1295,7 @@ or by the order of test execution. URLconf configuration ~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.urls +.. attribute:: SimpleTestCase.urls If your application provides views, you may want to include tests that use the test client to exercise those views. However, an end user is free to deploy the @@ -1282,9 +1304,9 @@ tests can't rely upon the fact that your views will be available at a particular URL. In order to provide a reliable URL space for your test, -``django.test.TestCase`` provides the ability to customize the URLconf +``django.test.*TestCase`` classes provide the ability to customize the URLconf configuration for the duration of the execution of a test suite. If your -``TestCase`` instance defines an ``urls`` attribute, the ``TestCase`` will use +``*TestCase`` instance defines an ``urls`` attribute, the ``*TestCase`` will use the value of that attribute as the :setting:`ROOT_URLCONF` for the duration of that test. @@ -1307,7 +1329,7 @@ URLconf for the duration of the test case. Multi-database support ~~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.multi_db +.. attribute:: TransactionTestCase.multi_db Django sets up a test database corresponding to every database that is defined in the :setting:`DATABASES` definition in your settings @@ -1340,12 +1362,12 @@ This test case will flush *all* the test databases before running Overriding settings ~~~~~~~~~~~~~~~~~~~ -.. method:: TestCase.settings +.. method:: SimpleTestCase.settings For testing purposes it's often useful to change a setting temporarily and revert to the original value after running the testing code. For this use case Django provides a standard Python context manager (see :pep:`343`) -:meth:`~django.test.TestCase.settings`, which can be used like this:: +:meth:`~django.test.SimpleTestCase.settings`, which can be used like this:: from django.test import TestCase @@ -1435,8 +1457,8 @@ MEDIA_ROOT, DEFAULT_FILE_STORAGE Default file storage Emptying the test outbox ~~~~~~~~~~~~~~~~~~~~~~~~ -If you use Django's custom ``TestCase`` class, the test runner will clear the -contents of the test email outbox at the start of each test case. +If you use any of Django's custom ``TestCase`` classes, the test runner will +clear thecontents of the test email outbox at the start of each test case. For more detail on email services during tests, see `Email services`_ below. @@ -1486,8 +1508,22 @@ your test suite. self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid email address.']}) +.. method:: SimpleTestCase.assertFormError(response, form, field, errors, msg_prefix='') + + Asserts that a field on a form raises the provided list of errors when + rendered on the form. + + ``form`` is the name the ``Form`` instance was given in the template + context. + + ``field`` is the name of the field on the form to check. If ``field`` + has a value of ``None``, non-field errors (errors you can access via + ``form.non_field_errors()``) will be checked. + + ``errors`` is an error string, or a list of error strings, that are + expected as a result of form validation. -.. method:: TestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) +.. method:: SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) Asserts that a ``Response`` instance produced the given ``status_code`` and that ``text`` appears in the content of the response. If ``count`` is @@ -1499,7 +1535,7 @@ your test suite. attribute ordering is not significant. See :meth:`~SimpleTestCase.assertHTMLEqual` for more details. -.. method:: TestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False) +.. method:: SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False) Asserts that a ``Response`` instance produced the given ``status_code`` and that ``text`` does not appears in the content of the response. @@ -1510,22 +1546,7 @@ your test suite. attribute ordering is not significant. See :meth:`~SimpleTestCase.assertHTMLEqual` for more details. -.. method:: TestCase.assertFormError(response, form, field, errors, msg_prefix='') - - Asserts that a field on a form raises the provided list of errors when - rendered on the form. - - ``form`` is the name the ``Form`` instance was given in the template - context. - - ``field`` is the name of the field on the form to check. If ``field`` - has a value of ``None``, non-field errors (errors you can access via - ``form.non_field_errors()``) will be checked. - - ``errors`` is an error string, or a list of error strings, that are - expected as a result of form validation. - -.. method:: TestCase.assertTemplateUsed(response, template_name, msg_prefix='') +.. method:: SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='') Asserts that the template with the given name was used in rendering the response. @@ -1539,15 +1560,15 @@ your test suite. with self.assertTemplateUsed(template_name='index.html'): render_to_string('index.html') -.. method:: TestCase.assertTemplateNotUsed(response, template_name, msg_prefix='') +.. method:: SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix='') Asserts that the template with the given name was *not* used in rendering the response. You can use this as a context manager in the same way as - :meth:`~TestCase.assertTemplateUsed`. + :meth:`~SimpleTestCase.assertTemplateUsed`. -.. method:: TestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') +.. method:: SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') Asserts that the response return a ``status_code`` redirect status, it redirected to ``expected_url`` (including any GET data), and the final @@ -1557,44 +1578,6 @@ your test suite. ``target_status_code`` will be the url and status code for the final point of the redirect chain. -.. method:: TestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) - - Asserts that a queryset ``qs`` returns a particular list of values ``values``. - - The comparison of the contents of ``qs`` and ``values`` is performed using - the function ``transform``; by default, this means that the ``repr()`` of - each value is compared. Any other callable can be used if ``repr()`` doesn't - provide a unique or helpful comparison. - - By default, the comparison is also ordering dependent. If ``qs`` doesn't - provide an implicit ordering, you can set the ``ordered`` parameter to - ``False``, which turns the comparison into a Python set comparison. - - .. versionchanged:: 1.6 - - The method now checks for undefined order and raises ``ValueError`` - if undefined order is spotted. The ordering is seen as undefined if - the given ``qs`` isn't ordered and the comparison is against more - than one ordered values. - -.. method:: TestCase.assertNumQueries(num, func, *args, **kwargs) - - Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that - ``num`` database queries are executed. - - If a ``"using"`` key is present in ``kwargs`` it is used as the database - alias for which to check the number of queries. If you wish to call a - function with a ``using`` parameter you can do it by wrapping the call with - a ``lambda`` to add an extra parameter:: - - self.assertNumQueries(7, lambda: my_function(using=7)) - - You can also use this as a context manager:: - - with self.assertNumQueries(2): - Person.objects.create(name="Aaron") - Person.objects.create(name="Daniel") - .. method:: SimpleTestCase.assertHTMLEqual(html1, html2, msg=None) Asserts that the strings ``html1`` and ``html2`` are equal. The comparison @@ -1624,6 +1607,8 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None) Asserts that the strings ``html1`` and ``html2`` are *not* equal. The @@ -1633,6 +1618,8 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None) .. versionadded:: 1.5 @@ -1644,6 +1631,8 @@ your test suite. syntax differences. When unvalid XML is passed in any parameter, an ``AssertionError`` is always raised, even if both string are identical. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None) .. versionadded:: 1.5 @@ -1652,6 +1641,68 @@ your test suite. comparison is based on XML semantics. See :meth:`~SimpleTestCase.assertXMLEqual` for details. + Output in case of error can be customized with the ``msg`` argument. + +.. method:: SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='') + + .. versionadded:: 1.5 + + Asserts that the HTML fragment ``needle`` is contained in the ``haystack`` one. + + If the ``count`` integer argument is specified, then additionally the number + of ``needle`` occurrences will be strictly verified. + + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid HTML. + +.. method:: SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None) + + .. versionadded:: 1.5 + + Asserts that the JSON fragments ``raw`` and ``expected_data`` are equal. + Usual JSON non-significant whitespace rules apply as the heavyweight is + delegated to the :mod:`json` library. + + Output in case of error can be customized with the ``msg`` argument. + +.. method:: TransactionTestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) + + Asserts that a queryset ``qs`` returns a particular list of values ``values``. + + The comparison of the contents of ``qs`` and ``values`` is performed using + the function ``transform``; by default, this means that the ``repr()`` of + each value is compared. Any other callable can be used if ``repr()`` doesn't + provide a unique or helpful comparison. + + By default, the comparison is also ordering dependent. If ``qs`` doesn't + provide an implicit ordering, you can set the ``ordered`` parameter to + ``False``, which turns the comparison into a Python set comparison. + + .. versionchanged:: 1.6 + + The method now checks for undefined order and raises ``ValueError`` + if undefined order is spotted. The ordering is seen as undefined if + the given ``qs`` isn't ordered and the comparison is against more + than one ordered values. + +.. method:: TransactionTestCase.assertNumQueries(num, func, *args, **kwargs) + + Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that + ``num`` database queries are executed. + + If a ``"using"`` key is present in ``kwargs`` it is used as the database + alias for which to check the number of queries. If you wish to call a + function with a ``using`` parameter you can do it by wrapping the call with + a ``lambda`` to add an extra parameter:: + + self.assertNumQueries(7, lambda: my_function(using=7)) + + You can also use this as a context manager:: + + with self.assertNumQueries(2): + Person.objects.create(name="Aaron") + Person.objects.create(name="Daniel") + .. _topics-testing-email: Email services @@ -1701,7 +1752,7 @@ and contents:: self.assertEqual(mail.outbox[0].subject, 'Subject here') As noted :ref:`previously `, the test outbox is emptied -at the start of every test in a Django ``TestCase``. To empty the outbox +at the start of every test in a Django ``*TestCase``. To empty the outbox manually, assign the empty list to ``mail.outbox``:: from django.core import mail -- cgit v1.3 From 56d6fdbbf50b29bd162fd5ab3296a25127d7af65 Mon Sep 17 00:00:00 2001 From: bbjay Date: Sat, 18 May 2013 18:50:58 +0200 Subject: Fixed #20452 -- Rename 'headers' to 'header fields'. --- docs/ref/request-response.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 2fac7f2f9c..fc26eabf1a 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -578,20 +578,20 @@ streaming response if (and only if) no middleware accesses the instantiated with an iterator. Django will consume and save the content of the iterator on first access. -Setting headers -~~~~~~~~~~~~~~~ +Setting header fields +~~~~~~~~~~~~~~~~~~~~~ -To set or remove a header in your response, treat it like a dictionary:: +To set or remove a header field in your response, treat it like a dictionary:: >>> response = HttpResponse() >>> response['Cache-Control'] = 'no-cache' >>> del response['Cache-Control'] Note that unlike a dictionary, ``del`` doesn't raise ``KeyError`` if the header -doesn't exist. +field doesn't exist. -HTTP headers cannot contain newlines. An attempt to set a header containing a -newline character (CR or LF) will raise ``BadHeaderError`` +HTTP header fields cannot contain newlines. An attempt to set a header field +containing a newline character (CR or LF) will raise ``BadHeaderError`` Telling the browser to treat the response as a file attachment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- cgit v1.3 From cb86f707a04e5635817d5f37a1443f9bf7d6af21 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 19 May 2013 12:58:13 +0200 Subject: Fixed #12747 -- Made reason phrases customizable. --- django/core/handlers/wsgi.py | 67 +++-------------------------------------- django/http/response.py | 70 +++++++++++++++++++++++++++++++++++++++++-- docs/ref/request-response.txt | 27 +++++++++++++---- docs/releases/1.6.txt | 2 ++ tests/responses/__init__.py | 0 tests/responses/models.py | 0 tests/responses/tests.py | 15 ++++++++++ 7 files changed, 109 insertions(+), 72 deletions(-) create mode 100644 tests/responses/__init__.py create mode 100644 tests/responses/models.py create mode 100644 tests/responses/tests.py (limited to 'docs/ref') diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index c348c6c8da..af78d1d269 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -13,66 +13,11 @@ from django.core.urlresolvers import set_script_prefix from django.utils import datastructures from django.utils.encoding import force_str, force_text, iri_to_uri -logger = logging.getLogger('django.request') +# For backwards compatibility -- lots of code uses this in the wild! +from django.http.response import REASON_PHRASES as STATUS_CODE_TEXT +logger = logging.getLogger('django.request') -# See http://www.iana.org/assignments/http-status-codes -STATUS_CODE_TEXT = { - 100: 'CONTINUE', - 101: 'SWITCHING PROTOCOLS', - 102: 'PROCESSING', - 200: 'OK', - 201: 'CREATED', - 202: 'ACCEPTED', - 203: 'NON-AUTHORITATIVE INFORMATION', - 204: 'NO CONTENT', - 205: 'RESET CONTENT', - 206: 'PARTIAL CONTENT', - 207: 'MULTI-STATUS', - 208: 'ALREADY REPORTED', - 226: 'IM USED', - 300: 'MULTIPLE CHOICES', - 301: 'MOVED PERMANENTLY', - 302: 'FOUND', - 303: 'SEE OTHER', - 304: 'NOT MODIFIED', - 305: 'USE PROXY', - 306: 'RESERVED', - 307: 'TEMPORARY REDIRECT', - 400: 'BAD REQUEST', - 401: 'UNAUTHORIZED', - 402: 'PAYMENT REQUIRED', - 403: 'FORBIDDEN', - 404: 'NOT FOUND', - 405: 'METHOD NOT ALLOWED', - 406: 'NOT ACCEPTABLE', - 407: 'PROXY AUTHENTICATION REQUIRED', - 408: 'REQUEST TIMEOUT', - 409: 'CONFLICT', - 410: 'GONE', - 411: 'LENGTH REQUIRED', - 412: 'PRECONDITION FAILED', - 413: 'REQUEST ENTITY TOO LARGE', - 414: 'REQUEST-URI TOO LONG', - 415: 'UNSUPPORTED MEDIA TYPE', - 416: 'REQUESTED RANGE NOT SATISFIABLE', - 417: 'EXPECTATION FAILED', - 418: "I'M A TEAPOT", - 422: 'UNPROCESSABLE ENTITY', - 423: 'LOCKED', - 424: 'FAILED DEPENDENCY', - 426: 'UPGRADE REQUIRED', - 500: 'INTERNAL SERVER ERROR', - 501: 'NOT IMPLEMENTED', - 502: 'BAD GATEWAY', - 503: 'SERVICE UNAVAILABLE', - 504: 'GATEWAY TIMEOUT', - 505: 'HTTP VERSION NOT SUPPORTED', - 506: 'VARIANT ALSO NEGOTIATES', - 507: 'INSUFFICIENT STORAGE', - 508: 'LOOP DETECTED', - 510: 'NOT EXTENDED', -} class LimitedStream(object): ''' @@ -254,11 +199,7 @@ class WSGIHandler(base.BaseHandler): response._handler_class = self.__class__ - try: - status_text = STATUS_CODE_TEXT[response.status_code] - except KeyError: - status_text = 'UNKNOWN STATUS CODE' - status = '%s %s' % (response.status_code, status_text) + status = '%s %s' % (response.status_code, response.reason_phrase) response_headers = [(str(k), str(v)) for k, v in response.items()] for c in response.cookies.values(): response_headers.append((str('Set-Cookie'), str(c.output(header='')))) diff --git a/django/http/response.py b/django/http/response.py index 88ac8848c2..671fb1c573 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -20,6 +20,65 @@ from django.utils.http import cookie_date from django.utils.six.moves import map +# See http://www.iana.org/assignments/http-status-codes +REASON_PHRASES = { + 100: 'CONTINUE', + 101: 'SWITCHING PROTOCOLS', + 102: 'PROCESSING', + 200: 'OK', + 201: 'CREATED', + 202: 'ACCEPTED', + 203: 'NON-AUTHORITATIVE INFORMATION', + 204: 'NO CONTENT', + 205: 'RESET CONTENT', + 206: 'PARTIAL CONTENT', + 207: 'MULTI-STATUS', + 208: 'ALREADY REPORTED', + 226: 'IM USED', + 300: 'MULTIPLE CHOICES', + 301: 'MOVED PERMANENTLY', + 302: 'FOUND', + 303: 'SEE OTHER', + 304: 'NOT MODIFIED', + 305: 'USE PROXY', + 306: 'RESERVED', + 307: 'TEMPORARY REDIRECT', + 400: 'BAD REQUEST', + 401: 'UNAUTHORIZED', + 402: 'PAYMENT REQUIRED', + 403: 'FORBIDDEN', + 404: 'NOT FOUND', + 405: 'METHOD NOT ALLOWED', + 406: 'NOT ACCEPTABLE', + 407: 'PROXY AUTHENTICATION REQUIRED', + 408: 'REQUEST TIMEOUT', + 409: 'CONFLICT', + 410: 'GONE', + 411: 'LENGTH REQUIRED', + 412: 'PRECONDITION FAILED', + 413: 'REQUEST ENTITY TOO LARGE', + 414: 'REQUEST-URI TOO LONG', + 415: 'UNSUPPORTED MEDIA TYPE', + 416: 'REQUESTED RANGE NOT SATISFIABLE', + 417: 'EXPECTATION FAILED', + 418: "I'M A TEAPOT", + 422: 'UNPROCESSABLE ENTITY', + 423: 'LOCKED', + 424: 'FAILED DEPENDENCY', + 426: 'UPGRADE REQUIRED', + 500: 'INTERNAL SERVER ERROR', + 501: 'NOT IMPLEMENTED', + 502: 'BAD GATEWAY', + 503: 'SERVICE UNAVAILABLE', + 504: 'GATEWAY TIMEOUT', + 505: 'HTTP VERSION NOT SUPPORTED', + 506: 'VARIANT ALSO NEGOTIATES', + 507: 'INSUFFICIENT STORAGE', + 508: 'LOOP DETECTED', + 510: 'NOT EXTENDED', +} + + class BadHeaderError(ValueError): pass @@ -33,8 +92,9 @@ class HttpResponseBase(six.Iterator): """ status_code = 200 + reason_phrase = None # Use default reason phrase for status code. - def __init__(self, content_type=None, status=None, mimetype=None): + def __init__(self, content_type=None, status=None, reason=None, mimetype=None): # _headers is a mapping of the lower-case name to the original case of # the header (required for working with legacy systems) and the header # value. Both the name of the header and its value are ASCII strings. @@ -53,9 +113,13 @@ class HttpResponseBase(six.Iterator): content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, self._charset) self.cookies = SimpleCookie() - if status: + if status is not None: self.status_code = status - + if reason is not None: + self.reason_phrase = reason + elif self.reason_phrase is None: + self.reason_phrase = REASON_PHRASES.get(self.status_code, + 'UNKNOWN STATUS CODE') self['Content-Type'] = content_type def serialize_headers(self): diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index fc26eabf1a..10c3f32e60 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -616,7 +616,13 @@ Attributes .. attribute:: HttpResponse.status_code - The `HTTP Status code`_ for the response. + The `HTTP status code`_ for the response. + +.. attribute:: HttpResponse.reason_phrase + + .. versionadded:: 1.6 + + The HTTP reason phrase for the response. .. attribute:: HttpResponse.streaming @@ -628,7 +634,7 @@ Attributes Methods ------- -.. method:: HttpResponse.__init__(content='', content_type=None, status=200) +.. method:: HttpResponse.__init__(content='', content_type=None, status=200, reason=None) Instantiates an ``HttpResponse`` object with the given page content and content type. @@ -646,8 +652,12 @@ Methods Historically, this parameter was called ``mimetype`` (now deprecated). - ``status`` is the `HTTP Status code`_ for the response. + ``status`` is the `HTTP status code`_ for the response. + + .. versionadded:: 1.6 + ``reason`` is the HTTP response phrase. If not provided, a default phrase + will be used. .. method:: HttpResponse.__setitem__(header, value) @@ -727,8 +737,7 @@ Methods This method makes an :class:`HttpResponse` instance a file-like object. -.. _HTTP Status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 - +.. _HTTP status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 .. _ref-httpresponse-subclasses: @@ -851,7 +860,13 @@ Attributes .. attribute:: HttpResponse.status_code - The `HTTP Status code`_ for the response. + The `HTTP status code`_ for the response. + +.. attribute:: HttpResponse.reason_phrase + + .. versionadded:: 1.6 + + The HTTP reason phrase for the response. .. attribute:: HttpResponse.streaming diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 0eab8540b0..b4668c38d0 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -241,6 +241,8 @@ Minor features * The ``choices`` argument to model fields now accepts an iterable of iterables instead of requiring an iterable of lists or tuples. +* The reason phrase can be customized in HTTP responses. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/responses/__init__.py b/tests/responses/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/responses/models.py b/tests/responses/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/responses/tests.py b/tests/responses/tests.py new file mode 100644 index 0000000000..e5320f5af9 --- /dev/null +++ b/tests/responses/tests.py @@ -0,0 +1,15 @@ +from django.http import HttpResponse +import unittest + +class HttpResponseTests(unittest.TestCase): + + def test_status_code(self): + resp = HttpResponse(status=418) + self.assertEqual(resp.status_code, 418) + self.assertEqual(resp.reason_phrase, "I'M A TEAPOT") + + def test_reason_phrase(self): + reason = "I'm an anarchist coffee pot on crack." + resp = HttpResponse(status=814, reason=reason) + self.assertEqual(resp.status_code, 814) + self.assertEqual(resp.reason_phrase, reason) -- cgit v1.3 From 660762681cfbd8cabce0b6c83fae5b3b60c0d60c Mon Sep 17 00:00:00 2001 From: Łukasz Langa Date: Sat, 18 May 2013 17:43:21 +0200 Subject: Fixed #20126 -- XViewMiddleware moved to django.contrib.admindocs.middleware --- django/contrib/admindocs/middleware.py | 23 +++++++++++++++++ django/middleware/doc.py | 27 ++++---------------- docs/ref/contrib/admin/admindocs.txt | 10 ++++---- docs/ref/middleware.txt | 13 ---------- docs/ref/settings.txt | 2 +- docs/releases/1.6.txt | 4 +++ tests/admin_docs/__init__.py | 0 tests/admin_docs/fixtures/data.xml | 17 +++++++++++++ tests/admin_docs/models.py | 0 tests/admin_docs/tests.py | 45 ++++++++++++++++++++++++++++++++++ tests/admin_docs/urls.py | 11 +++++++++ tests/admin_docs/views.py | 13 ++++++++++ 12 files changed, 124 insertions(+), 41 deletions(-) create mode 100644 django/contrib/admindocs/middleware.py create mode 100644 tests/admin_docs/__init__.py create mode 100644 tests/admin_docs/fixtures/data.xml create mode 100644 tests/admin_docs/models.py create mode 100644 tests/admin_docs/tests.py create mode 100644 tests/admin_docs/urls.py create mode 100644 tests/admin_docs/views.py (limited to 'docs/ref') diff --git a/django/contrib/admindocs/middleware.py b/django/contrib/admindocs/middleware.py new file mode 100644 index 0000000000..ee3fe2cb2f --- /dev/null +++ b/django/contrib/admindocs/middleware.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django import http + +class XViewMiddleware(object): + """ + Adds an X-View header to internal HEAD requests -- used by the documentation system. + """ + def process_view(self, request, view_func, view_args, view_kwargs): + """ + If the request method is HEAD and either the IP is internal or the + user is a logged-in staff member, quickly return with an x-header + indicating the view function. This is used by the documentation module + to lookup the view function for an arbitrary page. + """ + assert hasattr(request, 'user'), ( + "The XView middleware requires authentication middleware to be " + "installed. Edit your MIDDLEWARE_CLASSES setting to insert " + "'django.contrib.auth.middleware.AuthenticationMiddleware'.") + if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or + (request.user.is_active and request.user.is_staff)): + response = http.HttpResponse() + response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) + return response diff --git a/django/middleware/doc.py b/django/middleware/doc.py index ee3fe2cb2f..1af7b6150a 100644 --- a/django/middleware/doc.py +++ b/django/middleware/doc.py @@ -1,23 +1,6 @@ -from django.conf import settings -from django import http +"""XViewMiddleware has been moved to django.contrib.admindocs.middleware.""" -class XViewMiddleware(object): - """ - Adds an X-View header to internal HEAD requests -- used by the documentation system. - """ - def process_view(self, request, view_func, view_args, view_kwargs): - """ - If the request method is HEAD and either the IP is internal or the - user is a logged-in staff member, quickly return with an x-header - indicating the view function. This is used by the documentation module - to lookup the view function for an arbitrary page. - """ - assert hasattr(request, 'user'), ( - "The XView middleware requires authentication middleware to be " - "installed. Edit your MIDDLEWARE_CLASSES setting to insert " - "'django.contrib.auth.middleware.AuthenticationMiddleware'.") - if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or - (request.user.is_active and request.user.is_staff)): - response = http.HttpResponse() - response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) - return response +import warnings +warnings.warn(__doc__, PendingDeprecationWarning, stacklevel=2) + +from django.contrib.admindocs.middleware import XViewMiddleware diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt index 394d078e5b..4af94bdcf6 100644 --- a/docs/ref/contrib/admin/admindocs.txt +++ b/docs/ref/contrib/admin/admindocs.txt @@ -31,7 +31,7 @@ the following: * **Optional:** Linking to templates requires the :setting:`ADMIN_FOR` setting to be configured. * **Optional:** Using the admindocs bookmarklets requires the - :mod:`XViewMiddleware` to be installed. + :mod:`XViewMiddleware` to be installed. Once those steps are complete, you can start browsing the documentation by going to your admin interface and clicking the "Documentation" link in the @@ -156,7 +156,7 @@ Edit this object Using these bookmarklets requires that you are either logged into the :mod:`Django admin ` as a :class:`~django.contrib.auth.models.User` with -:attr:`~django.contrib.auth.models.User.is_staff` set to `True`, or -that the :mod:`django.middleware.doc` middleware and -:mod:`XViewMiddleware ` are installed and you -are accessing the site from an IP address listed in :setting:`INTERNAL_IPS`. +:attr:`~django.contrib.auth.models.User.is_staff` set to `True`, or that the +:mod:`XViewMiddleware ` is installed and +you are accessing the site from an IP address listed in +:setting:`INTERNAL_IPS`. diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 03885a2215..4898bab636 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -71,19 +71,6 @@ Adds a few conveniences for perfectionists: * Sends broken link notification emails to :setting:`MANAGERS` (see :doc:`/howto/error-reporting`). -View metadata middleware ------------------------- - -.. module:: django.middleware.doc - :synopsis: Middleware to help your app self-document. - -.. class:: XViewMiddleware - -Sends custom ``X-View`` HTTP headers to HEAD requests that come from IP -addresses defined in the :setting:`INTERNAL_IPS` setting. This is used by -Django's :doc:`automatic documentation system `. -Depends on :class:`~django.contrib.auth.middleware.AuthenticationMiddleware`. - GZip middleware --------------- diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 8ef59064f7..c1170e19c5 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1243,7 +1243,7 @@ Default: ``()`` (Empty tuple) A tuple of IP addresses, as strings, that: * See debug comments, when :setting:`DEBUG` is ``True`` -* Receive X headers if the ``XViewMiddleware`` is installed (see +* Receive X headers in admindocs if the ``XViewMiddleware`` is installed (see :doc:`/topics/http/middleware`) .. setting:: LANGUAGE_CODE diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index b4668c38d0..6643fb7d32 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -502,6 +502,10 @@ Miscellaneous ineffective so it has been removed, along with its generic implementation, previously available in ``django.core.xheaders``. +* The ``XViewMiddleware`` has been moved from ``django.middleware.doc`` to + ``django.contrib.admindocs.middleware`` because it is an implementation + detail of admindocs, proven not to be reusable in general. + Features deprecated in 1.6 ========================== diff --git a/tests/admin_docs/__init__.py b/tests/admin_docs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/admin_docs/fixtures/data.xml b/tests/admin_docs/fixtures/data.xml new file mode 100644 index 0000000000..aba8f4aace --- /dev/null +++ b/tests/admin_docs/fixtures/data.xml @@ -0,0 +1,17 @@ + + + + super + Super + User + super@example.com + sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158 + True + True + True + 2007-05-30 13:20:10 + 2007-05-30 13:20:10 + + + + diff --git a/tests/admin_docs/models.py b/tests/admin_docs/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/admin_docs/tests.py b/tests/admin_docs/tests.py new file mode 100644 index 0000000000..aeb527c7b9 --- /dev/null +++ b/tests/admin_docs/tests.py @@ -0,0 +1,45 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from django.test.utils import override_settings + + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class XViewMiddlewareTest(TestCase): + fixtures = ['data.xml'] + urls = 'admin_docs.urls' + + def test_xview_func(self): + user = User.objects.get(username='super') + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + self.client.login(username='super', password='secret') + response = self.client.head('/xview/func/') + self.assertTrue('X-View' in response) + self.assertEqual(response['X-View'], 'admin_docs.views.xview') + user.is_staff = False + user.save() + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + + def test_xview_class(self): + user = User.objects.get(username='super') + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) + self.client.login(username='super', password='secret') + response = self.client.head('/xview/class/') + self.assertTrue('X-View' in response) + self.assertEqual(response['X-View'], 'admin_docs.views.XViewClass') + user.is_staff = False + user.save() + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) diff --git a/tests/admin_docs/urls.py b/tests/admin_docs/urls.py new file mode 100644 index 0000000000..3c3a8fe5d8 --- /dev/null +++ b/tests/admin_docs/urls.py @@ -0,0 +1,11 @@ +# coding: utf-8 +from __future__ import absolute_import + +from django.conf.urls import patterns + +from . import views + +urlpatterns = patterns('', + (r'^xview/func/$', views.xview_dec(views.xview)), + (r'^xview/class/$', views.xview_dec(views.XViewClass.as_view())), +) diff --git a/tests/admin_docs/views.py b/tests/admin_docs/views.py new file mode 100644 index 0000000000..e47177c37f --- /dev/null +++ b/tests/admin_docs/views.py @@ -0,0 +1,13 @@ +from django.http import HttpResponse +from django.utils.decorators import decorator_from_middleware +from django.views.generic import View +from django.contrib.admindocs.middleware import XViewMiddleware + +xview_dec = decorator_from_middleware(XViewMiddleware) + +def xview(request): + return HttpResponse() + +class XViewClass(View): + def get(self, request): + return HttpResponse() -- cgit v1.3 From 7264e5c66110b6748b1d40ee3b0d511c71f3232f Mon Sep 17 00:00:00 2001 From: Silvan Spross Date: Sun, 19 May 2013 11:44:34 +0200 Subject: Add missing imports and models to the examples in the template layer documentation --- docs/howto/custom-template-tags.txt | 48 +++++++++++++++++++++++++------------ docs/ref/templates/api.txt | 7 ++++++ 2 files changed, 40 insertions(+), 15 deletions(-) (limited to 'docs/ref') diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 0d35654a04..f334c0f418 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -300,18 +300,21 @@ Template filter code falls into one of two situations: .. code-block:: python - from django.utils.html import conditional_escape - from django.utils.safestring import mark_safe - - @register.filter(needs_autoescape=True) - def initial_letter_filter(text, autoescape=None): - first, other = text[0], text[1:] - if autoescape: - esc = conditional_escape - else: - esc = lambda x: x - result = '%s%s' % (esc(first), esc(other)) - return mark_safe(result) + from django import template + from django.utils.html import conditional_escape + from django.utils.safestring import mark_safe + + register = template.Library() + + @register.filter(needs_autoescape=True) + def initial_letter_filter(text, autoescape=None): + first, other = text[0], text[1:] + if autoescape: + esc = conditional_escape + else: + esc = lambda x: x + result = '%s%s' % (esc(first), esc(other)) + return mark_safe(result) The ``needs_autoescape`` flag and the ``autoescape`` keyword argument mean that our function will know whether automatic escaping is in effect when the @@ -454,8 +457,9 @@ Continuing the above example, we need to define ``CurrentTimeNode``: .. code-block:: python - from django import template import datetime + from django import template + class CurrentTimeNode(template.Node): def __init__(self, format_string): self.format_string = format_string @@ -498,6 +502,8 @@ The ``__init__`` method for the ``Context`` class takes a parameter called .. code-block:: python + from django.template import Context + def render(self, context): # ... new_context = Context({'var': obj}, autoescape=context.autoescape) @@ -545,7 +551,10 @@ A naive implementation of ``CycleNode`` might look something like this: .. code-block:: python - class CycleNode(Node): + import itertools + from django import template + + class CycleNode(template.Node): def __init__(self, cyclevars): self.cycle_iter = itertools.cycle(cyclevars) def render(self, context): @@ -576,7 +585,7 @@ Let's refactor our ``CycleNode`` implementation to use the ``render_context``: .. code-block:: python - class CycleNode(Node): + class CycleNode(template.Node): def __init__(self, cyclevars): self.cyclevars = cyclevars def render(self, context): @@ -664,6 +673,7 @@ Now your tag should begin to look like this: .. code-block:: python from django import template + def do_format_time(parser, token): try: # split_contents() knows not to split quoted strings. @@ -722,6 +732,11 @@ Our earlier ``current_time`` function could thus be written like this: .. code-block:: python + import datetime + from django import template + + register = template.Library() + def current_time(format_string): return datetime.datetime.now().strftime(format_string) @@ -965,6 +980,9 @@ outputting it: .. code-block:: python + import datetime + from django import template + class CurrentTimeNode2(template.Node): def __init__(self, format_string): self.format_string = format_string diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 677aa13cbb..160cdc7194 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -286,6 +286,7 @@ fully-populated dictionary to ``Context()``. But you can add and delete items from a ``Context`` object once it's been instantiated, too, using standard dictionary syntax:: + >>> from django.template import Context >>> c = Context({"foo": "bar"}) >>> c['foo'] 'bar' @@ -397,6 +398,9 @@ Also, you can give ``RequestContext`` a list of additional processors, using the optional, third positional argument, ``processors``. In this example, the ``RequestContext`` instance gets a ``ip_address`` variable:: + from django.http import HttpResponse + from django.template import RequestContext + def ip_address_processor(request): return {'ip_address': request.META['REMOTE_ADDR']} @@ -417,6 +421,9 @@ optional, third positional argument, ``processors``. In this example, the :func:`~django.shortcuts.render_to_response()`: a ``RequestContext`` instance. Your code might look like this:: + from django.shortcuts import render_to_response + from django.template import RequestContext + def some_view(request): # ... return render_to_response('my_template.html', -- cgit v1.3 From 6a479955f0dcb37e04593bda715fd5f13c1f1106 Mon Sep 17 00:00:00 2001 From: Silvan Spross Date: Sun, 19 May 2013 12:22:40 +0200 Subject: Add missing imports and models to the examples in security documentation --- docs/ref/contrib/csrf.txt | 1 + 1 file changed, 1 insertion(+) (limited to 'docs/ref') diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index 968ef0b07b..9e58548376 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -384,6 +384,7 @@ Utilities the middleware. Example:: from django.views.decorators.csrf import csrf_exempt + from django.http import HttpResponse @csrf_exempt def my_view(request): -- cgit v1.3 From 08b501e7d314e9c45dd51d3ba27b2ecb0287df3b Mon Sep 17 00:00:00 2001 From: leandrafinger Date: Sun, 19 May 2013 11:15:35 +0200 Subject: add missing imports to the examples in the 'Forms' --- docs/ref/contrib/formtools/form-preview.txt | 1 + docs/ref/forms/api.txt | 101 +++++++++++++++------------- docs/ref/forms/fields.txt | 8 +++ docs/ref/forms/validation.txt | 9 +++ docs/ref/forms/widgets.txt | 3 + docs/topics/forms/formsets.txt | 26 +++++++ docs/topics/forms/media.txt | 5 ++ docs/topics/forms/modelforms.txt | 31 +++++++++ 8 files changed, 136 insertions(+), 48 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/contrib/formtools/form-preview.txt b/docs/ref/contrib/formtools/form-preview.txt index 011e72c2e0..b86cc4dc90 100644 --- a/docs/ref/contrib/formtools/form-preview.txt +++ b/docs/ref/contrib/formtools/form-preview.txt @@ -53,6 +53,7 @@ How to use ``FormPreview`` overrides the ``done()`` method:: from django.contrib.formtools.preview import FormPreview + from django.http import HttpResponseRedirect from myapp.models import SomeModel class SomeModelFormPreview(FormPreview): diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 34ed2e493e..67e3aab712 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -154,6 +154,7 @@ you include ``initial`` when instantiating the ``Form``, then the latter at the field level and at the form instance level, and the latter gets precedence:: + >>> from django import forms >>> class CommentForm(forms.Form): ... name = forms.CharField(initial='class') ... url = forms.URLField() @@ -238,6 +239,7 @@ When the ``Form`` is valid, ``cleaned_data`` will include a key and value for fields. In this example, the data dictionary doesn't include a value for the ``nick_name`` field, but ``cleaned_data`` includes it, with an empty value:: + >>> from django.forms import Form >>> class OptionalPersonForm(Form): ... first_name = CharField() ... last_name = CharField() @@ -327,54 +329,54 @@ a form object, and each rendering method returns a Unicode object. .. method:: Form.as_p - ``as_p()`` renders the form as a series of ``

`` tags, with each ``

`` - containing one field:: +``as_p()`` renders the form as a series of ``

`` tags, with each ``

`` +containing one field:: - >>> f = ContactForm() - >>> f.as_p() - u'

\n

\n

\n

' - >>> print(f.as_p()) -

-

-

-

+ >>> f = ContactForm() + >>> f.as_p() + u'

\n

\n

\n

' + >>> print(f.as_p()) +

+

+

+

``as_ul()`` ~~~~~~~~~~~ .. method:: Form.as_ul - ``as_ul()`` renders the form as a series of ``
  • `` tags, with each - ``
  • `` containing one field. It does *not* include the ``
      `` or - ``
    ``, so that you can specify any HTML attributes on the ``
      `` for - flexibility:: +``as_ul()`` renders the form as a series of ``
    • `` tags, with each +``
    • `` containing one field. It does *not* include the ``
        `` or +``
      ``, so that you can specify any HTML attributes on the ``
        `` for +flexibility:: - >>> f = ContactForm() - >>> f.as_ul() - u'
      • \n
      • \n
      • \n
      • ' - >>> print(f.as_ul()) -
      • -
      • -
      • -
      • + >>> f = ContactForm() + >>> f.as_ul() + u'
      • \n
      • \n
      • \n
      • ' + >>> print(f.as_ul()) +
      • +
      • +
      • +
      • ``as_table()`` ~~~~~~~~~~~~~~ .. method:: Form.as_table - Finally, ``as_table()`` outputs the form as an HTML ````. This is - exactly the same as ``print``. In fact, when you ``print`` a form object, - it calls its ``as_table()`` method behind the scenes:: +Finally, ``as_table()`` outputs the form as an HTML ``
        ``. This is +exactly the same as ``print``. In fact, when you ``print`` a form object, +it calls its ``as_table()`` method behind the scenes:: - >>> f = ContactForm() - >>> f.as_table() - u'\n\n\n' - >>> print(f.as_table()) - - - - + >>> f = ContactForm() + >>> f.as_table() + u'\n\n\n' + >>> print(f.as_table()) + + + + Styling required or erroneous form rows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -391,6 +393,8 @@ attributes to required rows or to rows with errors: simply set the :attr:`Form.error_css_class` and/or :attr:`Form.required_css_class` attributes:: + from django.forms import Form + class ContactForm(Form): error_css_class = 'error' required_css_class = 'required' @@ -621,23 +625,23 @@ For a field's list of errors, access the field's ``errors`` attribute. .. attribute:: BoundField.errors - A list-like object that is displayed as an HTML ``
          `` - when printed:: +A list-like object that is displayed as an HTML ``
            `` +when printed:: - >>> data = {'subject': 'hi', 'message': '', 'sender': '', 'cc_myself': ''} - >>> f = ContactForm(data, auto_id=False) - >>> print(f['message']) - - >>> f['message'].errors - [u'This field is required.'] - >>> print(f['message'].errors) -
            • This field is required.
            - >>> f['subject'].errors - [] - >>> print(f['subject'].errors) + >>> data = {'subject': 'hi', 'message': '', 'sender': '', 'cc_myself': ''} + >>> f = ContactForm(data, auto_id=False) + >>> print(f['message']) + + >>> f['message'].errors + [u'This field is required.'] + >>> print(f['message'].errors) +
            • This field is required.
            + >>> f['subject'].errors + [] + >>> print(f['subject'].errors) - >>> str(f['subject'].errors) - '' + >>> str(f['subject'].errors) + '' .. method:: BoundField.label_tag(contents=None, attrs=None) @@ -779,6 +783,7 @@ example, ``BeatleForm`` subclasses both ``PersonForm`` and ``InstrumentForm`` (in that order), and its field list includes the fields from the parent classes:: + >>> from django.forms import Form >>> class PersonForm(Form): ... first_name = CharField() ... last_name = CharField() diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 8e1a4b34d1..69e3aa71ad 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -48,6 +48,7 @@ By default, each ``Field`` class assumes the value is required, so if you pass an empty value -- either ``None`` or the empty string (``""``) -- then ``clean()`` will raise a ``ValidationError`` exception:: + >>> from django import forms >>> f = forms.CharField() >>> f.clean('foo') u'foo' @@ -107,6 +108,7 @@ behavior doesn't result in an adequate label. Here's a full example ``Form`` that implements ``label`` for two of its fields. We've specified ``auto_id=False`` to simplify the output:: + >>> from django import forms >>> class CommentForm(forms.Form): ... name = forms.CharField(label='Your name') ... url = forms.URLField(label='Your Web site', required=False) @@ -130,6 +132,7 @@ To specify dynamic initial data, see the :attr:`Form.initial` parameter. The use-case for this is when you want to display an "empty" form in which a field is initialized to a particular value. For example:: + >>> from django import forms >>> class CommentForm(forms.Form): ... name = forms.CharField(initial='Your name') ... url = forms.URLField(initial='http://') @@ -205,6 +208,7 @@ methods (e.g., ``as_ul()``). Here's a full example ``Form`` that implements ``help_text`` for two of its fields. We've specified ``auto_id=False`` to simplify the output:: + >>> from django import forms >>> class HelpTextContactForm(forms.Form): ... subject = forms.CharField(max_length=100, help_text='100 characters max.') ... message = forms.CharField() @@ -236,6 +240,7 @@ The ``error_messages`` argument lets you override the default messages that the field will raise. Pass in a dictionary with keys matching the error messages you want to override. For example, here is the default error message:: + >>> from django import forms >>> generic = forms.CharField() >>> generic.clean('') Traceback (most recent call last): @@ -853,6 +858,7 @@ Slightly complex built-in ``Field`` classes The list of fields that should be used to validate the field's value (in the order in which they are provided). + >>> from django.forms import ComboField >>> f = ComboField(fields=[CharField(max_length=20), EmailField()]) >>> f.clean('test@example.com') u'test@example.com' @@ -1001,6 +1007,8 @@ objects (in the case of ``ModelMultipleChoiceField``) into the object, and should return a string suitable for representing it. For example:: + from django.forms import ModelChoiceField + class MyModelChoiceField(ModelChoiceField): def label_from_instance(self, obj): return "My Object #%i" % obj.id diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index 3aaa69b6ea..87c9764f64 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -183,6 +183,9 @@ the ``default_validators`` attribute. Simple validators can be used to validate values inside the field, let's have a look at Django's ``SlugField``:: + from django.forms import CharField + from django.core import validators + class SlugField(CharField): default_validators = [validators.validate_slug] @@ -252,6 +255,8 @@ we want to make sure that the ``recipients`` field always contains the address don't want to put it into the general ``MultiEmailField`` class. Instead, we write a cleaning method that operates on the ``recipients`` field, like so:: + from django import forms + class ContactForm(forms.Form): # Everything as before. ... @@ -289,6 +294,8 @@ common method is to display the error at the top of the form. To create such an error, you can raise a ``ValidationError`` from the ``clean()`` method. For example:: + from django import forms + class ContactForm(forms.Form): # Everything as before. ... @@ -321,6 +328,8 @@ here and leaving it up to you and your designers to work out what works effectively in your particular situation. Our new code (replacing the previous sample) looks like this:: + from django import forms + class ContactForm(forms.Form): # Everything as before. ... diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 678f2e6949..0f6917d44c 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -201,6 +201,7 @@ foundation for custom widgets. .. code-block:: python + >>> from django import forms >>> name = forms.TextInput(attrs={'size': 10, 'title': 'Your name',}) >>> name.render('name', 'A name') u'' @@ -249,6 +250,8 @@ foundation for custom widgets. :class:`~datetime.datetime` value into a list with date and time split into two separate values:: + from django.forms import MultiWidget + class SplitDateTimeWidget(MultiWidget): # ... diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 9d77cd5274..e55c22e3a2 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -56,6 +56,9 @@ telling the formset how many additional forms to show in addition to the number of forms it generates from the initial data. Lets take a look at an example:: + >>> import datetime + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms imporrt ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, extra=2) >>> formset = ArticleFormSet(initial=[ ... {'title': u'Django is now open source', @@ -88,6 +91,8 @@ The ``max_num`` parameter to :func:`~django.forms.formsets.formset_factory` gives you the ability to limit the maximum number of empty forms the formset will display:: + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms imporrt ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1) >>> formset = ArticleFormSet() >>> for form in formset: @@ -124,6 +129,8 @@ 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 all forms in the formset:: + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms imporrt ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm) >>> data = { ... 'form-TOTAL_FORMS': u'1', @@ -230,6 +237,8 @@ A formset has a ``clean`` method similar to the one on a ``Form`` class. This is where you define your own validation that works at the formset level:: >>> from django.forms.formsets import BaseFormSet + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> class BaseArticleFormSet(BaseFormSet): ... def clean(self): @@ -276,6 +285,8 @@ If ``validate_max=True`` is passed to :func:`~django.forms.formsets.formset_factory`, validation will also check that the number of forms in the data set is less than or equal to ``max_num``. + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=True) >>> data = { ... 'form-TOTAL_FORMS': u'2', @@ -329,6 +340,8 @@ Default: ``False`` Lets you create a formset with the ability to order:: + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, can_order=True) >>> formset = ArticleFormSet(initial=[ ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, @@ -385,6 +398,8 @@ Default: ``False`` Lets you create a formset with the ability to delete:: + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True) >>> formset = ArticleFormSet(initial=[ ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, @@ -437,6 +452,9 @@ accomplished. The formset base class provides an ``add_fields`` method. You can simply override this method to add your own fields or even redefine the default fields/attributes of the order and deletion fields:: + >>> from django.forms.formsets import BaseFormSet + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> class BaseArticleFormSet(BaseFormSet): ... def add_fields(self, form, index): ... super(BaseArticleFormSet, self).add_fields(form, index) @@ -459,6 +477,10 @@ management form inside the template. Let's look at a sample view: .. code-block:: python + from django.forms.formsets import formset_factory + from django.shortcuts import render_to_response + from myapp.forms import ArticleForm + def manage_articles(request): ArticleFormSet = formset_factory(ArticleForm) if request.method == 'POST': @@ -534,6 +556,10 @@ a look at how this might be accomplished: .. code-block:: python + from django.forms.formsets import formset_factory + from django.shortcuts import render_to_response + from myapp.forms import ArticleForm, BookForm + def manage_articles(request): ArticleFormSet = formset_factory(ArticleForm) BookFormSet = formset_factory(BookForm) diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt index c0d63bb8cf..b014e97119 100644 --- a/docs/topics/forms/media.txt +++ b/docs/topics/forms/media.txt @@ -49,6 +49,8 @@ define the media requirements. Here's a simple example:: + from django import froms + class CalendarWidget(forms.TextInput): class Media: css = { @@ -211,6 +213,7 @@ to using :setting:`MEDIA_URL`. For example, if the :setting:`MEDIA_URL` for your site was ``'http://uploads.example.com/'`` and :setting:`STATIC_URL` was ``None``:: + >>> from django import forms >>> class CalendarWidget(forms.TextInput): ... class Media: ... css = { @@ -267,6 +270,7 @@ Combining media objects Media objects can also be added together. When two media objects are added, the resulting Media object contains the union of the media from both files:: + >>> from django import forms >>> class CalendarWidget(forms.TextInput): ... class Media: ... css = { @@ -298,6 +302,7 @@ Regardless of whether you define a media declaration, *all* Form objects have a media property. The default value for this property is the result of adding the media definitions for all widgets that are part of the form:: + >>> from django import forms >>> class ContactForm(forms.Form): ... date = DateField(widget=CalendarWidget) ... name = CharField(max_length=40, widget=OtherWidget) diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 3cd8c69ab5..6a445432d2 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -23,6 +23,7 @@ class from a Django model. For example:: >>> from django.forms import ModelForm + >>> from myapp.models import Article # Create the form class. >>> class ArticleForm(ModelForm): @@ -222,6 +223,9 @@ supplied, ``save()`` will update that instance. If it's not supplied, .. code-block:: python + >>> from myapp.models import Article + >>> from myapp.forms import ArticleForm + # Create a form instance from POST data. >>> f = ArticleForm(request.POST) @@ -316,6 +320,8 @@ these security concerns do not apply to you: 1. Set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used. For example:: + from django.forms import ModelForm + class AuthorForm(ModelForm): class Meta: model = Author @@ -401,6 +407,7 @@ of its default ````, you can override the field's widget:: from django.forms import ModelForm, Textarea + from myapp.models import Author class AuthorForm(ModelForm): class Meta: @@ -421,6 +428,9 @@ you can do this by declaratively specifying fields like you would in a regular For example, if you wanted to use ``MyDateFormField`` for the ``pub_date`` field, you could do the following:: + from django.forms import ModelForm + from myapp.models import Article + class ArticleForm(ModelForm): pub_date = MyDateFormField() @@ -432,6 +442,9 @@ field, you could do the following:: If you want to override a field's default label, then specify the ``label`` parameter when declaring the form field:: + from django.forms import ModelForm, DateField + from myapp.models import Article + class ArticleForm(ModelForm): pub_date = DateField(label='Publication date') @@ -484,6 +497,8 @@ By default, the fields in a ``ModelForm`` will not localize their data. To enable localization for fields, you can use the ``localized_fields`` attribute on the ``Meta`` class. + >>> from django.forms import ModelForm + >>> from myapp.models import Author >>> class AuthorForm(ModelForm): ... class Meta: ... model = Author @@ -574,6 +589,7 @@ definition. This may be more convenient if you do not have many customizations to make:: >>> from django.forms.models import modelform_factory + >>> from myapp.models import Book >>> BookForm = modelform_factory(Book, fields=("author", "title")) This can also be used to make simple modifications to existing forms, for @@ -604,6 +620,7 @@ of enhanced formset classes that make it easy to work with Django models. Let's reuse the ``Author`` model from above:: >>> from django.forms.models import modelformset_factory + >>> from myapp.models import Author >>> AuthorFormSet = modelformset_factory(Author) This will create a formset that is capable of working with the data associated @@ -642,6 +659,7 @@ Alternatively, you can create a subclass that sets ``self.queryset`` in ``__init__``:: from django.forms.models import BaseModelFormSet + from myapp.models import Author class BaseAuthorFormSet(BaseModelFormSet): def __init__(self, *args, **kwargs): @@ -787,6 +805,10 @@ Using a model formset in a view Model formsets are very similar to formsets. Let's say we want to present a formset to edit ``Author`` model instances:: + from django.forms.models import modelformset_factory + from django.shortcuts import render_to_response + from myapp.models import Author + def manage_authors(request): AuthorFormSet = modelformset_factory(Author) if request.method == 'POST': @@ -815,12 +837,15 @@ the unique constraints on your model (either ``unique``, ``unique_together`` or on a ``model_formset`` and maintain this validation, you must call the parent class's ``clean`` method:: + from django.forms.models import BaseModelFormSet + class MyModelFormSet(BaseModelFormSet): def clean(self): super(MyModelFormSet, self).clean() # example custom validation across forms in the formset: for form in self.forms: # your custom formset validation + pass Using a custom queryset ----------------------- @@ -828,6 +853,10 @@ Using a custom queryset As stated earlier, you can override the default queryset used by the model formset:: + from django.forms.models import modelformset_factory + from django.shortcuts import render_to_response + from myapp.models import Author + def manage_authors(request): AuthorFormSet = modelformset_factory(Author) if request.method == "POST": @@ -914,6 +943,8 @@ Inline formsets is a small abstraction layer on top of model formsets. These simplify the case of working with related objects via a foreign key. Suppose you have these two models:: + from django.db import models + class Author(models.Model): name = models.CharField(max_length=100) -- cgit v1.3 From 1fe587d80bfcf062a94252fb532c8ac035c833b9 Mon Sep 17 00:00:00 2001 From: vkuzma Date: Sun, 19 May 2013 12:42:35 +0200 Subject: Add missing imports and models to the examples in the admin documentation --- AUTHORS | 1 + docs/ref/contrib/admin/index.txt | 66 ++++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 12 deletions(-) (limited to 'docs/ref') diff --git a/AUTHORS b/AUTHORS index 7a249da5bf..846a4a4193 100644 --- a/AUTHORS +++ b/AUTHORS @@ -343,6 +343,7 @@ answer newbie questions, and generally made Django that much better: David Krauth Kevin Kubasik kurtiss@meetro.com + Vladimir Kuzma Denis Kuzmichyov Panos Laganakos Nick Lane diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 67e498ee91..b089416bfb 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -108,6 +108,8 @@ The ``ModelAdmin`` is very flexible. It has several options for dealing with customizing the interface. All options are defined on the ``ModelAdmin`` subclass:: + from django.contrib import admin + class AuthorAdmin(admin.ModelAdmin): date_hierarchy = 'pub_date' @@ -157,6 +159,8 @@ subclass:: For example, let's consider the following model:: + from django.db import models + class Author(models.Model): name = models.CharField(max_length=100) title = models.CharField(max_length=3) @@ -166,6 +170,8 @@ subclass:: and ``title`` fields, you would specify ``fields`` or ``exclude`` like this:: + from django.contrib import admin + class AuthorAdmin(admin.ModelAdmin): fields = ('name', 'title') @@ -234,6 +240,8 @@ subclass:: A full example, taken from the :class:`django.contrib.flatpages.models.FlatPage` model:: + from django.contrib import admin + class FlatPageAdmin(admin.ModelAdmin): fieldsets = ( (None, { @@ -356,6 +364,10 @@ subclass:: If your ``ModelForm`` and ``ModelAdmin`` both define an ``exclude`` option then ``ModelAdmin`` takes precedence:: + from django import forms + from django.contrib import admin + from myapp.models import Person + class PersonForm(forms.ModelForm): class Meta: @@ -459,6 +471,9 @@ subclass:: the same as the callable, but ``self`` in this context is the model instance. Here's a full model example:: + from django.db import models + from django.contrib import admin + class Person(models.Model): name = models.CharField(max_length=50) birthday = models.DateField() @@ -494,6 +509,8 @@ subclass:: Here's a full example model:: + from django.db import models + from django.contrib import admin from django.utils.html import format_html class Person(models.Model): @@ -519,6 +536,9 @@ subclass:: Here's a full example model:: + from django.db import models + from django.contrib import admin + class Person(models.Model): first_name = models.CharField(max_length=50) birthday = models.DateField() @@ -547,6 +567,8 @@ subclass:: For example:: + from django.db import models + from django.contrib import admin from django.utils.html import format_html class Person(models.Model): @@ -634,13 +656,13 @@ subclass:: ``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``, ``IntegerField``, ``ForeignKey`` or ``ManyToManyField``, for example:: - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): list_filter = ('is_staff', 'company') Field names in ``list_filter`` can also span relations using the ``__`` lookup, for example:: - class PersonAdmin(UserAdmin): + class PersonAdmin(admin.UserAdmin): list_filter = ('company__name',) * a class inheriting from ``django.contrib.admin.SimpleListFilter``, @@ -650,10 +672,10 @@ subclass:: from datetime import date + from django.contrib import admin from django.utils.translation import ugettext_lazy as _ - from django.contrib.admin import SimpleListFilter - class DecadeBornListFilter(SimpleListFilter): + class DecadeBornListFilter(admin.SimpleListFilter): # Human-readable title which will be displayed in the # right admin sidebar just above the filter options. title = _('decade born') @@ -689,7 +711,7 @@ subclass:: return queryset.filter(birthday__gte=date(1990, 1, 1), birthday__lte=date(1999, 12, 31)) - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): list_filter = (DecadeBornListFilter,) .. note:: @@ -732,11 +754,9 @@ subclass:: element is a class inheriting from ``django.contrib.admin.FieldListFilter``, for example:: - from django.contrib.admin import BooleanFieldListFilter - - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): list_filter = ( - ('is_staff', BooleanFieldListFilter), + ('is_staff', admin.BooleanFieldListFilter), ) .. note:: @@ -746,7 +766,7 @@ subclass:: It is possible to specify a custom template for rendering a list filter:: - class FilterWithCustomTemplate(SimpleListFilter): + class FilterWithCustomTemplate(admin.SimpleListFilter): template = "custom_template.html" See the default template provided by django (``admin/filter.html``) for @@ -876,10 +896,11 @@ subclass:: the admin interface to provide feedback on the status of the objects being edited, for example:: + from django.contrib import admin from django.utils.html import format_html_join from django.utils.safestring import mark_safe - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): readonly_fields = ('address_report',) def address_report(self, instance): @@ -1038,6 +1059,8 @@ templates used by the :class:`ModelAdmin` views: For example to attach ``request.user`` to the object prior to saving:: + from django.contrib import admin + class ArticleAdmin(admin.ModelAdmin): def save_model(self, request, obj, form, change): obj.user = request.user @@ -1071,7 +1094,7 @@ templates used by the :class:`ModelAdmin` views: is expected to return a ``list`` or ``tuple`` for ordering similar to the :attr:`ordering` attribute. For example:: - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): def get_ordering(self, request): if request.user.is_superuser: @@ -1298,6 +1321,8 @@ templates used by the :class:`ModelAdmin` views: Returns a :class:`~django.forms.ModelForm` class for use in the ``Formset`` on the changelist page. To use a custom form, for example:: + from django import forms + class MyForm(forms.ModelForm): pass @@ -1539,6 +1564,8 @@ information. The admin interface has the ability to edit models on the same page as a parent model. These are called inlines. Suppose you have these two models:: + from django.db import models + class Author(models.Model): name = models.CharField(max_length=100) @@ -1549,6 +1576,8 @@ information. You can edit the books authored by an author on the author page. You add inlines to a model by specifying them in a ``ModelAdmin.inlines``:: + from django.contrib import admin + class BookInline(admin.TabularInline): model = Book @@ -1682,6 +1711,8 @@ Working with a model with two or more foreign keys to the same parent model It is sometimes possible to have more than one foreign key to the same model. Take this model for instance:: + from django.db import models + class Friendship(models.Model): to_person = models.ForeignKey(Person, related_name="friends") from_person = models.ForeignKey(Person, related_name="from_friends") @@ -1690,6 +1721,9 @@ If you wanted to display an inline on the ``Person`` admin add/change pages you need to explicitly define the foreign key since it is unable to do so automatically:: + from django.contrib import admin + from myapp.models import Friendship + class FriendshipInline(admin.TabularInline): model = Friendship fk_name = "to_person" @@ -1712,6 +1746,8 @@ widgets with inlines. Suppose we have the following models:: + from django.db import models + class Person(models.Model): name = models.CharField(max_length=128) @@ -1722,6 +1758,8 @@ Suppose we have the following models:: If you want to display many-to-many relations using an inline, you can do so by defining an ``InlineModelAdmin`` object for the relationship:: + from django.contrib import admin + class MembershipInline(admin.TabularInline): model = Group.members.through @@ -1768,6 +1806,8 @@ However, we still want to be able to edit that information inline. Fortunately, this is easy to do with inline admin models. Suppose we have the following models:: + from django.db import models + class Person(models.Model): name = models.CharField(max_length=128) @@ -1816,6 +1856,8 @@ Using generic relations as an inline It is possible to use an inline with generically related objects. Let's say you have the following models:: + from django.db import models + class Image(models.Model): image = models.ImageField(upload_to="images") content_type = models.ForeignKey(ContentType) -- cgit v1.3 From 65f9e0affd8ca04e2c597c43c1547ef7c888ec2a Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Sun, 19 May 2013 14:15:36 +0200 Subject: Fixes #18896. Add tests verifying that you can get IntegrityErrors using get_or_create through relations like M2M, and it also adds a note into the documentation warning about it --- docs/ref/models/querysets.txt | 35 +++++++++++++++++++++++++++++++++++ tests/get_or_create/models.py | 9 +++++++++ tests/get_or_create/tests.py | 27 ++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) (limited to 'docs/ref') diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 9677b321c6..2dec00afc1 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1409,6 +1409,41 @@ has a side effect on your data. For more, see `Safe methods`_ in the HTTP spec. .. _Safe methods: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 +.. warning:: + + You can use ``get_or_create()`` through :class:`~django.db.models.ManyToManyField` + attributes and reverse relations. In that case you will restrict the queries + inside the context of that relation. That could lead you to some integrity + problems if you don't use it consistently. + + Being the following models:: + + class Chapter(models.Model): + title = models.CharField(max_length=255, unique=True) + + class Book(models.Model): + title = models.CharField(max_length=256) + chapters = models.ManyToManyField(Chapter) + + You can use ``get_or_create()`` through Book's chapters field, but it only + fetches inside the context of that book:: + + >>> book = Book.objects.create(title="Ulysses") + >>> book.chapters.get_or_create(title="Telemachus") + (, True) + >>> book.chapters.get_or_create(title="Telemachus") + (, False) + >>> Chapter.objects.create(title="Chapter 1") + + >>> book.chapters.get_or_create(title="Chapter 1") + # Raises IntegrityError + + This is happening because it's trying to get or create "Chapter 1" through the + book "Ulysses", but it can't do any of them: the relation can't fetch that + chapter because it isn't related to that book, but it can't create it either + because ``title`` field should be unique. + + bulk_create ~~~~~~~~~~~ diff --git a/tests/get_or_create/models.py b/tests/get_or_create/models.py index 82905de4f8..2f21344f59 100644 --- a/tests/get_or_create/models.py +++ b/tests/get_or_create/models.py @@ -28,3 +28,12 @@ class ManualPrimaryKeyTest(models.Model): class Profile(models.Model): person = models.ForeignKey(Person, primary_key=True) + + +class Tag(models.Model): + text = models.CharField(max_length=256, unique=True) + + +class Thing(models.Model): + name = models.CharField(max_length=256) + tags = models.ManyToManyField(Tag) diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index e9cce9bbde..5117a2f915 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -6,7 +6,7 @@ import traceback from django.db import IntegrityError from django.test import TestCase, TransactionTestCase -from .models import Person, ManualPrimaryKeyTest, Profile +from .models import Person, ManualPrimaryKeyTest, Profile, Tag, Thing class GetOrCreateTests(TestCase): @@ -77,3 +77,28 @@ class GetOrCreateTransactionTests(TransactionTestCase): pass else: self.skipTest("This backend does not support integrity checks.") + + +class GetOrCreateThroughManyToMany(TestCase): + + def test_get_get_or_create(self): + tag = Tag.objects.create(text='foo') + a_thing = Thing.objects.create(name='a') + a_thing.tags.add(tag) + obj, created = a_thing.tags.get_or_create(text='foo') + + self.assertFalse(created) + self.assertEqual(obj.pk, tag.pk) + + def test_create_get_or_create(self): + a_thing = Thing.objects.create(name='a') + obj, created = a_thing.tags.get_or_create(text='foo') + + self.assertTrue(created) + self.assertEqual(obj.text, 'foo') + self.assertIn(obj, a_thing.tags.all()) + + def test_something(self): + Tag.objects.create(text='foo') + a_thing = Thing.objects.create(name='a') + self.assertRaises(IntegrityError, a_thing.tags.get_or_create, text='foo') -- cgit v1.3 From 2d309a7043e3625cfeeadbc252322e5599dfffc0 Mon Sep 17 00:00:00 2001 From: Bozidar Benko Date: Sun, 19 May 2013 10:52:29 +0200 Subject: Fixed #15961 -- Modified ModelAdmin to allow for custom search methods. This adds a get_search_results method that users can override to provide custom search strategies. Thanks to Daniele Procida for help with the docs. --- AUTHORS | 1 + django/contrib/admin/options.py | 31 ++++++++++++++++++++++++++++++- django/contrib/admin/views/main.py | 32 +++++--------------------------- docs/ref/contrib/admin/index.txt | 36 ++++++++++++++++++++++++++++++++++++ tests/admin_views/admin.py | 17 ++++++++++++++++- tests/admin_views/models.py | 6 ++++++ tests/admin_views/tests.py | 16 +++++++++++++++- 7 files changed, 109 insertions(+), 30 deletions(-) (limited to 'docs/ref') diff --git a/AUTHORS b/AUTHORS index 771e5e6270..ad5cea2f39 100644 --- a/AUTHORS +++ b/AUTHORS @@ -99,6 +99,7 @@ answer newbie questions, and generally made Django that much better: Brian Beck Shannon -jj Behrens Esdras Beleza + Božidar Benko Chris Bennett Danilo Bargen Shai Berger diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index e7edccd585..f27ed3b653 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1,4 +1,5 @@ import copy +import operator from functools import update_wrapper, partial from django import forms @@ -9,7 +10,7 @@ from django.forms.models import (modelform_factory, modelformset_factory, from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets, helpers from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects, - model_format_dict, NestedObjects) + model_format_dict, NestedObjects, lookup_needs_distinct) from django.contrib.admin import validation from django.contrib.admin.templatetags.admin_static import static from django.contrib import messages @@ -255,6 +256,34 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): """ return self.prepopulated_fields + def get_search_results(self, request, queryset, search_term): + # Apply keyword searches. + def construct_search(field_name): + if field_name.startswith('^'): + return "%s__istartswith" % field_name[1:] + elif field_name.startswith('='): + return "%s__iexact" % field_name[1:] + elif field_name.startswith('@'): + return "%s__search" % field_name[1:] + else: + return "%s__icontains" % field_name + + use_distinct = False + if self.search_fields and search_term: + orm_lookups = [construct_search(str(search_field)) + for search_field in self.search_fields] + for bit in search_term.split(): + or_queries = [models.Q(**{orm_lookup: bit}) + for orm_lookup in orm_lookups] + queryset = queryset.filter(reduce(operator.or_, or_queries)) + if not use_distinct: + for search_spec in orm_lookups: + if lookup_needs_distinct(self.opts, search_spec): + use_distinct = True + break + + return queryset, use_distinct + def get_queryset(self, request): """ Returns a QuerySet of all model instances that can be edited by the diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 050d4776d0..21ac30b7b3 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,7 +1,5 @@ -import operator import sys import warnings -from functools import reduce from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured from django.core.paginator import InvalidPage @@ -331,7 +329,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): def get_queryset(self, request): # First, we collect all the declared list filters. (self.filter_specs, self.has_filters, remaining_lookup_params, - use_distinct) = self.get_filters(request) + filters_use_distinct) = self.get_filters(request) # Then, we let every list filter modify the queryset to its liking. qs = self.root_queryset @@ -378,31 +376,11 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): ordering = self.get_ordering(request, qs) qs = qs.order_by(*ordering) - # Apply keyword searches. - def construct_search(field_name): - if field_name.startswith('^'): - return "%s__istartswith" % field_name[1:] - elif field_name.startswith('='): - return "%s__iexact" % field_name[1:] - elif field_name.startswith('@'): - return "%s__search" % field_name[1:] - else: - return "%s__icontains" % field_name - - if self.search_fields and self.query: - orm_lookups = [construct_search(str(search_field)) - for search_field in self.search_fields] - for bit in self.query.split(): - or_queries = [models.Q(**{orm_lookup: bit}) - for orm_lookup in orm_lookups] - qs = qs.filter(reduce(operator.or_, or_queries)) - if not use_distinct: - for search_spec in orm_lookups: - if lookup_needs_distinct(self.lookup_opts, search_spec): - use_distinct = True - break + # Apply search results + qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query) - if use_distinct: + # Remove duplicates from results, if neccesary + if filters_use_distinct | search_use_distinct: return qs.distinct() else: return qs diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index b089416bfb..90570f9576 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1005,6 +1005,9 @@ subclass:: Performs a full-text match. This is like the default search method but uses an index. Currently this is only available for MySQL. + If you need to customize search you can use :meth:`ModelAdmin.get_search_results` to provide additional or alternate + search behaviour. + Custom template options ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1102,6 +1105,39 @@ templates used by the :class:`ModelAdmin` views: else: return ['name'] +.. method:: ModelAdmin.get_search_results(self, request, queryset, search_term) + + .. versionadded:: 1.6 + + The ``get_search_results`` method modifies the list of objects displayed in + to those that match the provided search term. It accepts the request, a + queryset that applies the current filters, and the user-provided search term. + It returns a tuple containing a queryset modified to implement the search, and + a boolean indicating if the results may contain duplicates. + + The default implementation searches the fields named in :attr:`ModelAdmin.search_fields`. + + This method may be overridden with your own custom search method. For + example, you might wish to search by an integer field, or use an external + tool such as Solr or Haystack. You must establish if the queryset changes + implemented by your search method may introduce duplicates into the results, + and return ``True`` in the second element of the return value. + + For example, to enable search by integer field, you could use:: + + class PersonAdmin(admin.ModelAdmin): + list_display = ('name', 'age') + search_fields = ('name',) + + def get_search_results(self, request, queryset, search_term): + queryset, use_distinct = super(PersonAdmin, self).get_search_results(request, queryset, search_term) + try: + search_term_as_int = int(search_term) + queryset |= self.model.objects.filter(age=search_term_as_int) + except: + pass + return queryset, use_distinct + .. method:: ModelAdmin.save_related(self, request, form, formsets, change) The ``save_related`` method is given the ``HttpRequest``, the parent diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index cc7585cd2d..4e68ffb8a6 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -24,7 +24,7 @@ from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, Gadget, Villain, SuperVillain, Plot, PlotDetails, CyclicOne, CyclicTwo, WorkHour, Reservation, FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory, Book, Promo, ChapterXtra1, Pizza, Topping, - Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, + Album, Question, Answer, ComplexSortedPerson, PluggableSearchPerson, PrePopulatedPostLargeSlug, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, RelatedPrepopulated, UndeletableObject, UserMessenger, Simple, Choice, @@ -530,6 +530,20 @@ class ComplexSortedPersonAdmin(admin.ModelAdmin): colored_name.admin_order_field = 'name' +class PluggableSearchPersonAdmin(admin.ModelAdmin): + list_display = ('name', 'age') + search_fields = ('name',) + + def get_search_results(self, request, queryset, search_term): + queryset, use_distinct = super(PluggableSearchPersonAdmin, self).get_search_results(request, queryset, search_term) + try: + search_term_as_int = int(search_term) + queryset |= self.model.objects.filter(age=search_term_as_int) + except: + pass + return queryset, use_distinct + + class AlbumAdmin(admin.ModelAdmin): list_filter = ['title'] @@ -733,6 +747,7 @@ site.register(Question) site.register(Answer) site.register(PrePopulatedPost, PrePopulatedPostAdmin) site.register(ComplexSortedPerson, ComplexSortedPersonAdmin) +site.register(PluggableSearchPerson, PluggableSearchPersonAdmin) site.register(PrePopulatedPostLargeSlug, PrePopulatedPostLargeSlugAdmin) site.register(AdminOrderedField, AdminOrderedFieldAdmin) site.register(AdminOrderedModelMethod, AdminOrderedModelMethodAdmin) diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 1916949f63..e78dc40a6c 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -591,6 +591,12 @@ class ComplexSortedPerson(models.Model): age = models.PositiveIntegerField() is_employee = models.NullBooleanField() + +class PluggableSearchPerson(models.Model): + name = models.CharField(max_length=100) + age = models.PositiveIntegerField() + + class PrePopulatedPostLargeSlug(models.Model): """ Regression test for #15938: a large max_length for the slugfield must not diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 8e678a72b3..91dc5b4344 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -46,7 +46,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, DooHickey, FancyDoodad, Whatsit, Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee, Question, Answer, Inquisition, Actor, FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, - OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField, + OtherStory, ComplexSortedPerson, PluggableSearchPerson, Parent, Child, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, Simple, UndeletableObject, Choice, ShortMessage, Telegram) @@ -2202,6 +2202,20 @@ class AdminSearchTest(TestCase): self.assertContains(response, "\n0 persons\n") self.assertNotContains(response, "Guido") + def test_pluggable_search(self): + p1 = PluggableSearchPerson.objects.create(name="Bob", age=10) + p2 = PluggableSearchPerson.objects.create(name="Amy", age=20) + + response = self.client.get('/test_admin/admin/admin_views/pluggablesearchperson/?q=Bob') + # confirm the search returned one object + self.assertContains(response, "\n1 pluggable search person\n") + self.assertContains(response, "Bob") + + response = self.client.get('/test_admin/admin/admin_views/pluggablesearchperson/?q=20') + # confirm the search returned one object + self.assertContains(response, "\n1 pluggable search person\n") + self.assertContains(response, "Amy") + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminInheritedInlinesTest(TestCase): -- cgit v1.3 From 6786920fd8a1dfa43bba8333548c2496847298af Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sun, 19 May 2013 12:39:14 +0200 Subject: Fixed #16330 -- added --pks option in dumpdata command Thanks to guettli for the initial ticket and patch, with additional work from mehmetakyuz and Kevin Brolly. --- django/core/management/commands/dumpdata.py | 21 ++++++++++++-- docs/ref/django-admin.txt | 9 ++++++ docs/releases/1.6.txt | 4 +++ tests/fixtures/tests.py | 44 +++++++++++++++++++++++++++-- 4 files changed, 74 insertions(+), 4 deletions(-) (limited to 'docs/ref') diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index d3650b1eb8..b1e06e4255 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -21,6 +21,8 @@ class Command(BaseCommand): help='Use natural keys if they are available.'), make_option('-a', '--all', action='store_true', dest='use_base_manager', default=False, help="Use Django's base manager to dump all models stored in the database, including those that would otherwise be filtered or modified by a custom manager."), + make_option('--pks', dest='primary_keys', action='append', default=[], + help="Only dump objects with given primary keys. Accepts a comma seperated list of keys. This option will only work when you specify one model."), ) help = ("Output the contents of the database as a fixture of the given " "format (using each model's default manager unless --all is " @@ -37,6 +39,12 @@ class Command(BaseCommand): show_traceback = options.get('traceback') use_natural_keys = options.get('use_natural_keys') use_base_manager = options.get('use_base_manager') + pks = options.get('primary_keys') + + if pks: + primary_keys = pks.split(',') + else: + primary_keys = False excluded_apps = set() excluded_models = set() @@ -55,8 +63,12 @@ class Command(BaseCommand): raise CommandError('Unknown app in excludes: %s' % exclude) if len(app_labels) == 0: + if primary_keys: + raise CommandError("You can only use --pks option with one model") app_list = SortedDict((app, None) for app in get_apps() if app not in excluded_apps) else: + if len(app_labels) > 1 and primary_keys: + raise CommandError("You can only use --pks option with one model") app_list = SortedDict() for label in app_labels: try: @@ -77,6 +89,8 @@ class Command(BaseCommand): else: app_list[app] = [model] except ValueError: + if primary_keys: + raise CommandError("You can only use --pks option with one model") # This is just an app - no model qualifier app_label = label try: @@ -107,8 +121,11 @@ class Command(BaseCommand): objects = model._base_manager else: objects = model._default_manager - for obj in objects.using(using).\ - order_by(model._meta.pk.name).iterator(): + + queryset = objects.using(using).order_by(model._meta.pk.name) + if primary_keys: + queryset = queryset.filter(pk__in=primary_keys) + for obj in queryset.iterator(): yield obj try: diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 2f2880679c..e193a448d2 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -227,6 +227,15 @@ a natural key definition. If you are dumping ``contrib.auth`` ``Permission`` objects or ``contrib.contenttypes`` ``ContentType`` objects, you should probably be using this flag. +.. versionadded:: 1.6 + +.. django-admin-option:: --pks + +By default, ``dumpdata`` will output all the records of the model, but +you can use the ``--pks`` option to specify a comma seperated list of +primary keys on which to filter. This is only available when dumping +one model. + flush ----- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index a6244498f2..9950717420 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -249,6 +249,10 @@ Minor features and :func:`~django.contrib.auth.views.password_change`, you can now pass URL names and they will be resolved. +* The ``dumpdata`` manage.py command now has a --pks option which will + allow users to specify the primary keys of objects they want to dump. + This option can only be used with one model. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index f954933046..e32522b929 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -27,14 +27,15 @@ class TestCaseFixtureLoadingTests(TestCase): class DumpDataAssertMixin(object): def _dumpdata_assert(self, args, output, format='json', natural_keys=False, - use_base_manager=False, exclude_list=[]): + use_base_manager=False, exclude_list=[], primary_keys=[]): new_io = six.StringIO() management.call_command('dumpdata', *args, **{'format': format, 'stdout': new_io, 'stderr': new_io, 'use_natural_keys': natural_keys, 'use_base_manager': use_base_manager, - 'exclude': exclude_list}) + 'exclude': exclude_list, + 'primary_keys': primary_keys}) command_output = new_io.getvalue().strip() if format == "json": self.assertJSONEqual(command_output, output) @@ -223,6 +224,45 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): # even those normally filtered by the manager self._dumpdata_assert(['fixtures.Spy'], '[{"pk": %d, "model": "fixtures.spy", "fields": {"cover_blown": true}}, {"pk": %d, "model": "fixtures.spy", "fields": {"cover_blown": false}}]' % (spy2.pk, spy1.pk), use_base_manager=True) + def test_dumpdata_with_pks(self): + management.call_command('loaddata', 'fixture1.json', verbosity=0, commit=False) + management.call_command('loaddata', 'fixture2.json', verbosity=0, commit=False) + self._dumpdata_assert( + ['fixtures.Article'], + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + + self._dumpdata_assert( + ['fixtures.Article'], + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}]', + primary_keys='2' + ) + + with six.assertRaisesRegex(self, management.CommandError, + "You can only use --pks option with one model"): + self._dumpdata_assert( + ['fixtures'], + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + + with six.assertRaisesRegex(self, management.CommandError, + "You can only use --pks option with one model"): + self._dumpdata_assert( + '', + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + + with six.assertRaisesRegex(self, management.CommandError, + "You can only use --pks option with one model"): + self._dumpdata_assert( + ['fixtures.Article', 'fixtures.category'], + '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + def test_compress_format_loading(self): # Load fixture 4 (compressed), using format specification management.call_command('loaddata', 'fixture4.json', verbosity=0, commit=False) -- cgit v1.3 From e83ff42792eb52235cacda58f3441673cc4e4c94 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 19 May 2013 12:30:53 -0400 Subject: Fixed #20459 - Improved example for setting HTTP header fields. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Jérémie Blaser. --- AUTHORS | 1 + docs/ref/request-response.txt | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) (limited to 'docs/ref') diff --git a/AUTHORS b/AUTHORS index ad5cea2f39..564284598c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -111,6 +111,7 @@ answer newbie questions, and generally made Django that much better: Paul Bissex Loïc Bistuer Simon Blanchard + Jérémie Blaser Craig Blaszczyk David Blewett Artem Gnilov diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 10c3f32e60..9ff97e87d0 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -584,12 +584,19 @@ Setting header fields To set or remove a header field in your response, treat it like a dictionary:: >>> response = HttpResponse() - >>> response['Cache-Control'] = 'no-cache' - >>> del response['Cache-Control'] + >>> response['Age'] = 120 + >>> del response['Age'] Note that unlike a dictionary, ``del`` doesn't raise ``KeyError`` if the header field doesn't exist. +For setting the ``Cache-Control`` and ``Vary`` header fields, it is recommended +to use the :meth:`~django.utils.cache.patch_cache_control` and +:meth:`~django.utils.cache.patch_vary_headers` methods from +:mod:`django.utils.cache`, since these fields can have multiple, comma-separated +values. The "patch" methods ensure that other values, e.g. added by a +middleware, are not removed. + HTTP header fields cannot contain newlines. An attempt to set a header field containing a newline character (CR or LF) will raise ``BadHeaderError`` -- cgit v1.3 From 428875775c9f0d318081a30612feacee0ad5ed9e Mon Sep 17 00:00:00 2001 From: Alasdair Nicol Date: Mon, 20 May 2013 00:59:17 +0200 Subject: Fix typo in redirect view docs --- docs/ref/class-based-views/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/ref') diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index ee0bf0f225..94bf6e12f9 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -208,7 +208,7 @@ RedirectView urlpatterns = patterns('', - url(r'r^(?P\d+)/$', ArticleCounterRedirectView.as_view(), name='article-counter'), + url(r'^(?P\d+)/$', ArticleCounterRedirectView.as_view(), name='article-counter'), url(r'^go-to-django/$', RedirectView.as_view(url='http://djangoproject.com'), name='go-to-django'), ) -- cgit v1.3 From d9d24c4521866247f1e5fc1150f5cb9b57979863 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 20 May 2013 10:17:49 -0400 Subject: Fixed warnings in admindocs; refs #20126. --- docs/ref/contrib/admin/admindocs.txt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt index 4af94bdcf6..6deb7bdbf8 100644 --- a/docs/ref/contrib/admin/admindocs.txt +++ b/docs/ref/contrib/admin/admindocs.txt @@ -30,8 +30,8 @@ the following: * Install the docutils Python module (http://docutils.sf.net/). * **Optional:** Linking to templates requires the :setting:`ADMIN_FOR` setting to be configured. -* **Optional:** Using the admindocs bookmarklets requires the - :mod:`XViewMiddleware` to be installed. +* **Optional:** Using the admindocs bookmarklets requires + ``django.contrib.admindocs.middleware.XViewMiddleware`` to be installed. Once those steps are complete, you can start browsing the documentation by going to your admin interface and clicking the "Documentation" link in the @@ -157,6 +157,5 @@ Using these bookmarklets requires that you are either logged into the :mod:`Django admin ` as a :class:`~django.contrib.auth.models.User` with :attr:`~django.contrib.auth.models.User.is_staff` set to `True`, or that the -:mod:`XViewMiddleware ` is installed and -you are accessing the site from an IP address listed in -:setting:`INTERNAL_IPS`. +``XViewMiddleware`` is installed and you are accessing the site from an IP +address listed in :setting:`INTERNAL_IPS`. -- cgit v1.3 From a542b808baf49ede4d40b2893f1bb74cd60d56f6 Mon Sep 17 00:00:00 2001 From: Łukasz Langa Date: Mon, 20 May 2013 22:50:54 +0200 Subject: Removed a confusing duplicate SESSION_COOKIE_DOMAIN header The note is clearly a part of MESSAGE_STORAGE documentation. As a separate section, it broke automatic link generation on the HTML version of the documentation. --- docs/ref/settings.txt | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index c1170e19c5..b8ebc16bad 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2231,6 +2231,9 @@ Controls where Django stores message data. Valid values are: See :ref:`message storage backends ` for more details. +The backends that use cookies -- ``CookieStorage`` and ``FallbackStorage`` -- +use the value of :setting:`SESSION_COOKIE_DOMAIN` when setting their cookies. + .. setting:: MESSAGE_TAGS MESSAGE_TAGS @@ -2262,18 +2265,6 @@ to override. See :ref:`message-displaying` above for more details. according to the values in the above :ref:`constants table `. -.. _messages-session_cookie_domain: - -SESSION_COOKIE_DOMAIN ---------------------- - -Default: ``None`` - -The storage backends that use cookies -- ``CookieStorage`` and -``FallbackStorage`` -- use the value of :setting:`SESSION_COOKIE_DOMAIN` in -setting their cookies. - - .. _settings-sessions: Sessions -- cgit v1.3 From cec9558fba1bc6401ea2ec6d71b816b4dfd31b28 Mon Sep 17 00:00:00 2001 From: Wiktor Kolodziej Date: Tue, 21 May 2013 13:03:45 +0200 Subject: Fixed #17308 -- Enabled the use of short_description on properties in the admin. --- django/contrib/admin/util.py | 10 ++++++++-- docs/ref/contrib/admin/index.txt | 23 ++++++++++++++++++++++- tests/admin_util/tests.py | 14 ++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) (limited to 'docs/ref') diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 97858e688e..078adbe827 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -269,8 +269,9 @@ def lookup_field(name, obj, model_admin=None): def label_for_field(name, model, model_admin=None, return_attr=False): """ - Returns a sensible label for a field name. The name can be a callable or the - name of an object attributes, as well as a genuine fields. If return_attr is + Returns a sensible label for a field name. The name can be a callable, + property (but not created with @property decorator) or the name of an + object's attribute, as well as a genuine fields. If return_attr is True, the resolved attribute (which could be a callable) is also returned. This will be None if (and only if) the name refers to a field. """ @@ -303,6 +304,10 @@ def label_for_field(name, model, model_admin=None, return_attr=False): if hasattr(attr, "short_description"): label = attr.short_description + elif (isinstance(attr, property) and + hasattr(attr, "fget") and + hasattr(attr.fget, "short_description")): + label = attr.fget.short_description elif callable(attr): if attr.__name__ == "": label = "--" @@ -315,6 +320,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False): else: return label + def help_text_for_field(name, model): try: help_text = model._meta.get_field_by_name(name)[0].help_text diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 90570f9576..0aec62f7b9 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -464,7 +464,7 @@ subclass:: list_display = ('upper_case_name',) def upper_case_name(self, obj): - return ("%s %s" % (obj.first_name, obj.last_name)).upper() + return ("%s %s" % (obj.first_name, obj.last_name)).upper() upper_case_name.short_description = 'Name' * A string representing an attribute on the model. This behaves almost @@ -589,6 +589,27 @@ subclass:: The above will tell Django to order by the ``first_name`` field when trying to sort by ``colored_first_name`` in the admin. + * Elements of ``list_display`` can also be properties. Please note however, + that due to the way properties work in Python, setting + ``short_description`` on a property is only possible when using the + ``property()`` function and **not** with the ``@property`` decorator. + + For example:: + + class Person(object): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + + def my_property(self): + return self.first_name + ' ' + self.last_name + my_property.short_description = "Full name of the person" + + full_name = property(my_property) + + class PersonAdmin(admin.ModelAdmin): + list_display = ('full_name',) + + * .. versionadded:: 1.6 The field names in ``list_display`` will also appear as CSS classes in diff --git a/tests/admin_util/tests.py b/tests/admin_util/tests.py index 7898f200b5..35b7681cbb 100644 --- a/tests/admin_util/tests.py +++ b/tests/admin_util/tests.py @@ -236,6 +236,20 @@ class UtilTests(unittest.TestCase): ("not Really the Model", MockModelAdmin.test_from_model) ) + def test_label_for_property(self): + # NOTE: cannot use @property decorator, because of + # AttributeError: 'property' object has no attribute 'short_description' + class MockModelAdmin(object): + def my_property(self): + return "this if from property" + my_property.short_description = 'property short description' + test_from_property = property(my_property) + + self.assertEqual( + label_for_field("test_from_property", Article, model_admin=MockModelAdmin), + 'property short description' + ) + def test_related_name(self): """ Regression test for #13963 -- cgit v1.3 From ea9a0857d4922fab1f9146f3a7828b67281edc89 Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Tue, 21 May 2013 18:35:12 +0300 Subject: Fixed #19326 -- Added first() and last() methods to QuerySet --- django/db/models/manager.py | 6 ++++++ django/db/models/query.py | 20 ++++++++++++++++++++ docs/ref/models/querysets.txt | 30 ++++++++++++++++++++++++++++++ docs/releases/1.6.txt | 5 +++++ tests/get_earliest_or_latest/tests.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+) (limited to 'docs/ref') diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 43a8264f11..a1aa79f809 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -186,6 +186,12 @@ class Manager(six.with_metaclass(RenameManagerMethods)): def latest(self, *args, **kwargs): return self.get_queryset().latest(*args, **kwargs) + def first(self): + return self.get_queryset().first() + + def last(self): + return self.get_queryset().last() + def order_by(self, *args, **kwargs): return self.get_queryset().order_by(*args, **kwargs) diff --git a/django/db/models/query.py b/django/db/models/query.py index 4313d044ee..f2015f57a8 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -498,6 +498,26 @@ class QuerySet(object): def latest(self, field_name=None): return self._earliest_or_latest(field_name=field_name, direction="-") + def first(self): + """ + Returns the first object of a query, returns None if no match is found. + """ + qs = self if self.ordered else self.order_by('pk') + try: + return qs[0] + except IndexError: + return None + + def last(self): + """ + Returns the last object of a query, returns None if no match is found. + """ + qs = self.reverse() if self.ordered else self.order_by('-pk') + try: + return qs[0] + except IndexError: + return None + def in_bulk(self, id_list): """ Returns a dictionary mapping each of the given IDs to the object with diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 2dec00afc1..b9a4037440 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1585,6 +1585,36 @@ earliest Works otherwise like :meth:`~django.db.models.query.QuerySet.latest` except the direction is changed. +first +~~~~~ +.. method:: first() + +.. versionadded:: 1.6 + +Returns the first object matched by the queryset, or ``None`` if there +is no matching object. If the ``QuerySet`` has no ordering defined, then the +queryset is automatically ordered by the primary key. + +Example:: + + p = Article.objects.order_by('title', 'pub_date').first() + +Note that ``first()`` is a convenience method, the following code sample is +equivalent to the above example:: + + try: + p = Article.objects.order_by('title', 'pub_date')[0] + except IndexError: + p = None + +last +~~~~ +.. method:: last() + +.. versionadded:: 1.6 + +Works like :meth:`first()` except the ordering is reversed. + aggregate ~~~~~~~~~ diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index a417e81f62..29b4569d4e 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -253,6 +253,11 @@ Minor features allow users to specify the primary keys of objects they want to dump. This option can only be used with one model. +* Added ``QuerySet`` methods :meth:`~django.db.models.query.QuerySet.first` + and :meth:`~django.db.models.query.QuerySet.last` which are convenience + methods returning the first or last object matching the filters. Returns + ``None`` if there are no objects matching. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/get_earliest_or_latest/tests.py b/tests/get_earliest_or_latest/tests.py index 6317a0974c..8d16af9587 100644 --- a/tests/get_earliest_or_latest/tests.py +++ b/tests/get_earliest_or_latest/tests.py @@ -121,3 +121,34 @@ class EarliestOrLatestTests(TestCase): p2 = Person.objects.create(name="Stephanie", birthday=datetime(1960, 2, 3)) self.assertRaises(AssertionError, Person.objects.latest) self.assertEqual(Person.objects.latest("birthday"), p2) + + def test_first(self): + p1 = Person.objects.create(name="Bob", birthday=datetime(1950, 1, 1)) + p2 = Person.objects.create(name="Alice", birthday=datetime(1961, 2, 3)) + self.assertEqual( + Person.objects.first(), p1) + self.assertEqual( + Person.objects.order_by('name').first(), p2) + self.assertEqual( + Person.objects.filter(birthday__lte=datetime(1955, 1, 1)).first(), + p1) + self.assertIs( + Person.objects.filter(birthday__lte=datetime(1940, 1, 1)).first(), + None) + + def test_last(self): + p1 = Person.objects.create( + name="Alice", birthday=datetime(1950, 1, 1)) + p2 = Person.objects.create( + name="Bob", birthday=datetime(1960, 2, 3)) + # Note: by default PK ordering. + self.assertEqual( + Person.objects.last(), p2) + self.assertEqual( + Person.objects.order_by('-name').last(), p1) + self.assertEqual( + Person.objects.filter(birthday__lte=datetime(1955, 1, 1)).last(), + p1) + self.assertIs( + Person.objects.filter(birthday__lte=datetime(1940, 1, 1)).last(), + None) -- cgit v1.3 From 375e275030574dbe4ff52aa2ca36f698d6c9fca8 Mon Sep 17 00:00:00 2001 From: Selwin Ong Date: Tue, 21 May 2013 23:18:35 +0700 Subject: Slightly reworded 'last()' docs. --- docs/ref/models/querysets.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/ref') diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index b9a4037440..53359441ae 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1613,7 +1613,7 @@ last .. versionadded:: 1.6 -Works like :meth:`first()` except the ordering is reversed. +Works like :meth:`first()`, but returns the last object in the queryset. aggregate ~~~~~~~~~ -- cgit v1.3 From 5d164569916b44e476a3fa2a292c6fe9c6735177 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 21 May 2013 21:29:14 +0200 Subject: Fixed #20476 -- Typo. --- docs/ref/class-based-views/index.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/ref') diff --git a/docs/ref/class-based-views/index.txt b/docs/ref/class-based-views/index.txt index a027953416..821edc0874 100644 --- a/docs/ref/class-based-views/index.txt +++ b/docs/ref/class-based-views/index.txt @@ -32,7 +32,7 @@ A class-based view is deployed into a URL pattern using the .. admonition:: Thread safety with view arguments Arguments passed to a view are shared between every instance of a view. - This means that you shoudn't use a list, dictionary, or any other + This means that you shouldn't use a list, dictionary, or any other mutable object as an argument to a view. If you do and the shared object is modified, the actions of one user visiting your view could have an effect on subsequent users visiting the same view. -- cgit v1.3 From ee8b810b977572e39dc6acf4d13cc5e05f4d65ee Mon Sep 17 00:00:00 2001 From: Krzysztof Jurewicz Date: Tue, 21 May 2013 18:01:29 +0200 Subject: Fixed #20478 – Added support for HTTP PATCH method in generic views. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- django/views/generic/base.py | 5 ++++- docs/ref/class-based-views/base.txt | 2 +- docs/releases/1.6.txt | 3 +++ tests/generic_views/test_base.py | 6 ++++++ 4 files changed, 14 insertions(+), 2 deletions(-) (limited to 'docs/ref') diff --git a/django/views/generic/base.py b/django/views/generic/base.py index d50d6bbc55..286a18d0f2 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -30,7 +30,7 @@ class View(object): dispatch-by-method and simple sanity checking. """ - http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] + http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] def __init__(self, **kwargs): """ @@ -206,3 +206,6 @@ class RedirectView(View): def put(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 94bf6e12f9..17862978e7 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -55,7 +55,7 @@ View Default:: - ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] + ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] **Methods** diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index a1a29471a3..3bc5a0996c 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -258,6 +258,9 @@ Minor features methods returning the first or last object matching the filters. Returns ``None`` if there are no objects matching. +* :class:`~django.views.generic.base.View` and + :class:`~django.views.generic.base.RedirectView` now support HTTP PATCH method. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index 9080015f4f..ffd9b1b480 100644 --- a/tests/generic_views/test_base.py +++ b/tests/generic_views/test_base.py @@ -384,6 +384,12 @@ class RedirectViewTest(unittest.TestCase): self.assertEqual(response.status_code, 301) self.assertEqual(response.url, '/bar/') + def test_redirect_PATCH(self): + "Default is a permanent redirect" + response = RedirectView.as_view(url='/bar/')(self.rf.patch('/foo/')) + self.assertEqual(response.status_code, 301) + self.assertEqual(response.url, '/bar/') + def test_redirect_DELETE(self): "Default is a permanent redirect" response = RedirectView.as_view(url='/bar/')(self.rf.delete('/foo/')) -- cgit v1.3 From 8c2c178a093513240107b1bda22a3ea48bf32865 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 22 May 2013 08:51:16 -0400 Subject: Fixed a broken link introduced in a542b808baf. --- docs/ref/contrib/messages.txt | 1 - 1 file changed, 1 deletion(-) (limited to 'docs/ref') diff --git a/docs/ref/contrib/messages.txt b/docs/ref/contrib/messages.txt index 0a376bca18..608c37bb7f 100644 --- a/docs/ref/contrib/messages.txt +++ b/docs/ref/contrib/messages.txt @@ -373,4 +373,3 @@ behavior: * :setting:`MESSAGE_LEVEL` * :setting:`MESSAGE_STORAGE` * :setting:`MESSAGE_TAGS` -* :ref:`SESSION_COOKIE_DOMAIN` -- cgit v1.3 From 3de1288042f2dc1cb8a2b36ae0fc4d9e0beb6494 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Fri, 17 May 2013 18:18:35 -0400 Subject: Fixed #11398 - Added a pre_syncdb signal --- django/core/management/commands/flush.py | 2 +- django/core/management/commands/syncdb.py | 6 ++- django/core/management/sql.py | 14 ++++++ django/db/models/signals.py | 1 + docs/ref/signals.txt | 47 ++++++++++++++++++ tests/syncdb_signals/__init__.py | 0 tests/syncdb_signals/models.py | 11 +++++ tests/syncdb_signals/tests.py | 79 +++++++++++++++++++++++++++++++ 8 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 tests/syncdb_signals/__init__.py create mode 100644 tests/syncdb_signals/models.py create mode 100644 tests/syncdb_signals/tests.py (limited to 'docs/ref') diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index 10066417a1..c56fc1e1b0 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -20,7 +20,7 @@ class Command(NoArgsCommand): default=DEFAULT_DB_ALIAS, help='Nominates a database to flush. ' 'Defaults to the "default" database.'), make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True, - help='Tells Django not to load any initial data after database synchronization.'), + help='Tells Django not to load any initial data after database synchronization.'), ) help = ('Returns the database to the state it was in immediately after ' 'syncdb was executed. This means that all data will be removed ' diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index ab80d8aece..3e73d24a04 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -1,11 +1,12 @@ from optparse import make_option +import itertools import traceback from django.conf import settings from django.core.management import call_command from django.core.management.base import NoArgsCommand from django.core.management.color import no_style -from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal +from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS from django.utils.datastructures import SortedDict from django.utils.importlib import import_module @@ -80,6 +81,9 @@ class Command(NoArgsCommand): for app_name, model_list in all_models ) + create_models = set([x for x in itertools.chain(*manifest.values())]) + emit_pre_sync_signal(create_models, verbosity, interactive, db) + # Create the tables for each model if verbosity >= 1: self.stdout.write("Creating tables ...\n") diff --git a/django/core/management/sql.py b/django/core/management/sql.py index ac60ed470c..27ada10248 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -137,6 +137,7 @@ def sql_indexes(app, style, connection): output.extend(connection.creation.sql_indexes_for_model(model, style)) return output + def sql_destroy_indexes(app, style, connection): "Returns a list of the DROP INDEX SQL statements for all models in the given app." output = [] @@ -191,6 +192,19 @@ def custom_sql_for_model(model, style, connection): return output +def emit_pre_sync_signal(create_models, verbosity, interactive, db): + # Emit the pre_sync signal for every application. + for app in models.get_apps(): + app_name = app.__name__.split('.')[-2] + if verbosity >= 2: + print("Running pre-sync handlers for application %s" % app_name) + models.signals.pre_syncdb.send(sender=app, app=app, + create_models=create_models, + verbosity=verbosity, + interactive=interactive, + db=db) + + def emit_post_sync_signal(created_models, verbosity, interactive, db): # Emit the post_sync signal for every application. for app in models.get_apps(): diff --git a/django/db/models/signals.py b/django/db/models/signals.py index 09f93d0f77..3e321893c1 100644 --- a/django/db/models/signals.py +++ b/django/db/models/signals.py @@ -12,6 +12,7 @@ post_save = Signal(providing_args=["instance", "raw", "created", "using", "updat pre_delete = Signal(providing_args=["instance", "using"], use_caching=True) post_delete = Signal(providing_args=["instance", "using"], use_caching=True) +pre_syncdb = Signal(providing_args=["app", "create_models", "verbosity", "interactive", "db"]) post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"], use_caching=True) m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True) diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index ca472bd60e..e7270e1957 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -360,6 +360,53 @@ Management signals Signals sent by :doc:`django-admin `. +pre_syncdb +---------- + +.. data:: django.db.models.signals.pre_syncdb + :module: + +Sent by the :djadmin:`syncdb` command before it starts to install an +application. + +Any handlers that listen to this signal need to be written in a particular +place: a ``management`` module in one of your :setting:`INSTALLED_APPS`. If +handlers are registered anywhere else they may not be loaded by +:djadmin:`syncdb`. + +Arguments sent with this signal: + +``sender`` + The ``models`` module that was just installed. That is, if + :djadmin:`syncdb` just installed an app called ``"foo.bar.myapp"``, + ``sender`` will be the ``foo.bar.myapp.models`` module. + +``app`` + Same as ``sender``. + +``create_models`` + A list of the model classes from any app which :djadmin:`syncdb` plans to + create. + + +``verbosity`` + Indicates how much information manage.py is printing on screen. See + the :djadminopt:`--verbosity` flag for details. + + Functions which listen for :data:`pre_syncdb` should adjust what they + output to the screen based on the value of this argument. + +``interactive`` + If ``interactive`` is ``True``, it's safe to prompt the user to input + things on the command line. If ``interactive`` is ``False``, functions + which listen for this signal should not try to prompt for anything. + + For example, the :mod:`django.contrib.auth` app only prompts to create a + superuser when ``interactive`` is ``True``. + +``db`` + The alias of database on which a command will operate. + post_syncdb ----------- diff --git a/tests/syncdb_signals/__init__.py b/tests/syncdb_signals/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/syncdb_signals/models.py b/tests/syncdb_signals/models.py new file mode 100644 index 0000000000..c41d993e94 --- /dev/null +++ b/tests/syncdb_signals/models.py @@ -0,0 +1,11 @@ +# from django.db import models + + +# class Author(models.Model): +# name = models.CharField(max_length=100) + +# class Meta: +# ordering = ['name'] + +# def __unicode__(self): +# return self.name diff --git a/tests/syncdb_signals/tests.py b/tests/syncdb_signals/tests.py new file mode 100644 index 0000000000..fd2d442d08 --- /dev/null +++ b/tests/syncdb_signals/tests.py @@ -0,0 +1,79 @@ +from django.db import connections +from django.db.models import signals +from django.test import TestCase +from django.core import management +from django.utils import six + +from shared_models import models + + +PRE_SYNCDB_ARGS = ['app', 'create_models', 'verbosity', 'interactive', 'db'] +SYNCDB_DATABASE = 'default' +SYNCDB_VERBOSITY = 1 +SYNCDB_INTERACTIVE = False + + +class PreSyncdbReceiver(object): + def __init__(self): + self.call_counter = 0 + self.call_args = None + + def __call__(self, signal, sender, **kwargs): + self.call_counter = self.call_counter + 1 + self.call_args = kwargs + + +class OneTimeReceiver(object): + """ + Special receiver for handle the fact that test runner calls syncdb for + several databases and several times for some of them. + """ + + def __init__(self): + self.call_counter = 0 + self.call_args = None + self.tables = None # list of tables at the time of the call + + def __call__(self, signal, sender, **kwargs): + # Although test runner calls syncdb for several databases, + # testing for only one of them is quite sufficient. + if kwargs['db'] == SYNCDB_DATABASE: + self.call_counter = self.call_counter + 1 + self.call_args = kwargs + connection = connections[SYNCDB_DATABASE] + self.tables = connection.introspection.table_names() + # we need to test only one call of syncdb + signals.pre_syncdb.disconnect(pre_syncdb_receiver, sender=models) + + +# We connect receiver here and not in unit test code because we need to +# connect receiver before test runner creates database. That is, sequence of +# actions would be: +# +# 1. Test runner imports this module. +# 2. We connect receiver. +# 3. Test runner calls syncdb for create default database. +# 4. Test runner execute our unit test code. +pre_syncdb_receiver = OneTimeReceiver() +signals.pre_syncdb.connect(pre_syncdb_receiver, sender=models) + + +class SyncdbSignalTests(TestCase): + def test_pre_syncdb_call_time(self): + self.assertEqual(pre_syncdb_receiver.call_counter, 1) + self.assertFalse(pre_syncdb_receiver.tables) + + def test_pre_syncdb_args(self): + r = PreSyncdbReceiver() + signals.pre_syncdb.connect(r, sender=models) + management.call_command('syncdb', database=SYNCDB_DATABASE, + verbosity=SYNCDB_VERBOSITY, interactive=SYNCDB_INTERACTIVE, + load_initial_data=False, stdout=six.StringIO()) + + args = r.call_args + self.assertEqual(r.call_counter, 1) + self.assertEqual(set(args), set(PRE_SYNCDB_ARGS)) + self.assertEqual(args['app'], models) + self.assertEqual(args['verbosity'], SYNCDB_VERBOSITY) + self.assertEqual(args['interactive'], SYNCDB_INTERACTIVE) + self.assertEqual(args['db'], 'default') -- cgit v1.3 From b664cb818d2e5896df2763299ea2c61a9af069a8 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Thu, 23 May 2013 14:00:17 +0200 Subject: Fixed #19237 (again) - Made strip_tags consistent between Python versions --- django/utils/html.py | 14 +++++++------- docs/ref/utils.txt | 10 ++++++++-- tests/utils_tests/test_html.py | 3 +++ 3 files changed, 18 insertions(+), 9 deletions(-) (limited to 'docs/ref') diff --git a/django/utils/html.py b/django/utils/html.py index 573235092d..0d28c77a61 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -16,7 +16,7 @@ from django.utils.functional import allow_lazy from django.utils import six from django.utils.text import normalize_newlines -from .html_parser import HTMLParser +from .html_parser import HTMLParser, HTMLParseError # Configuration for urlize() function. @@ -136,13 +136,13 @@ class MLStripper(HTMLParser): def strip_tags(value): """Returns the given HTML with all tags stripped.""" s = MLStripper() - s.feed(value) - data = s.get_data() try: - res = s.close() - except Exception as e: - data += s.rawdata - return data + s.feed(value) + s.close() + except HTMLParseError: + return value + else: + return s.get_data() strip_tags = allow_lazy(strip_tags) def remove_tags(html, tags): diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 14ae9aa9b8..bf14af0855 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -490,7 +490,7 @@ Atom1Feed Usually you should build up HTML using Django's templates to make use of its autoescape mechanism, using the utilities in :mod:`django.utils.safestring` -where appropriate. This module provides some additional low level utilitiesfor +where appropriate. This module provides some additional low level utilities for escaping HTML. .. function:: escape(text) @@ -564,7 +564,13 @@ escaping HTML. strip_tags(value) If ``value`` is ``"Joel a slug"`` the - return value will be ``"Joel is a slug"``. + return value will be ``"Joel is a slug"``. Note that ``strip_tags`` result + may still contain unsafe HTML content, so you might use + :func:`~django.utils.html.escape` to make it a safe string. + + .. versionchanged:: 1.6 + + For improved safety, ``strip_tags`` is now parser-based. .. function:: remove_tags(value, tags) diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py index c3e9f7c878..b973f1c64f 100644 --- a/tests/utils_tests/test_html.py +++ b/tests/utils_tests/test_html.py @@ -70,6 +70,9 @@ class TestUtilsHtml(TestCase): ('a', 'a'), ('e', 'e'), ('hi, b2!', 'b7>b2!'), ('b', 'b'), ('a

            b

            c', 'abc'), -- cgit v1.3 From 48424adaba74379eee311b3d1519f011212357ad Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Mon, 20 May 2013 11:51:05 -0600 Subject: Fixed #17648 -- Add `for_concrete_model` to `GenericForeignKey`. Allows a `GenericForeignKey` to reference proxy models. The default for `for_concrete_model` is `True` to keep backwards compatibility. Also added the analog `for_concrete_model` kwarg to `generic_inlineformset_factory` to provide an API at the form level. --- AUTHORS | 1 + django/contrib/contenttypes/generic.py | 32 ++++++--- docs/ref/contrib/contenttypes.txt | 17 ++++- docs/releases/1.6.txt | 5 ++ tests/generic_relations/models.py | 19 +++++ tests/generic_relations/tests.py | 122 +++++++++++++++++++++++++++++++-- 6 files changed, 179 insertions(+), 17 deletions(-) (limited to 'docs/ref') diff --git a/AUTHORS b/AUTHORS index e963c0914f..b8047d92c9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -598,6 +598,7 @@ answer newbie questions, and generally made Django that much better: Milton Waddams Chris Wagner Rick Wagner + Gavin Wahl wam-djangobug@wamber.net Wang Chun Filip Wasilewski diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 399d24aa87..5a19f35e07 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -35,9 +35,10 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)): fields. """ - def __init__(self, ct_field="content_type", fk_field="object_id"): + def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True): self.ct_field = ct_field self.fk_field = fk_field + self.for_concrete_model = for_concrete_model def contribute_to_class(self, cls, name): self.name = name @@ -63,7 +64,8 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)): def get_content_type(self, obj=None, id=None, using=None): if obj is not None: - return ContentType.objects.db_manager(obj._state.db).get_for_model(obj) + return ContentType.objects.db_manager(obj._state.db).get_for_model( + obj, for_concrete_model=self.for_concrete_model) elif id: return ContentType.objects.db_manager(using).get_for_id(id) else: @@ -160,6 +162,8 @@ class GenericRelation(ForeignObject): self.object_id_field_name = kwargs.pop("object_id_field", "object_id") self.content_type_field_name = kwargs.pop("content_type_field", "content_type") + self.for_concrete_model = kwargs.pop("for_concrete_model", True) + kwargs['blank'] = True kwargs['editable'] = False kwargs['serialize'] = False @@ -201,7 +205,7 @@ class GenericRelation(ForeignObject): # Save a reference to which model this class is on for future use self.model = cls # Add the descriptor for the relation - setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self)) + setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self, self.for_concrete_model)) def contribute_to_related_class(self, cls, related): pass @@ -216,7 +220,8 @@ class GenericRelation(ForeignObject): """ Returns the content type associated with this field's model. """ - return ContentType.objects.get_for_model(self.model) + return ContentType.objects.get_for_model(self.model, + for_concrete_model=self.for_concrete_model) def get_extra_restriction(self, where_class, alias, remote_alias): field = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0] @@ -232,7 +237,8 @@ class GenericRelation(ForeignObject): """ return self.rel.to._base_manager.db_manager(using).filter(**{ "%s__pk" % self.content_type_field_name: - ContentType.objects.db_manager(using).get_for_model(self.model).pk, + ContentType.objects.db_manager(using).get_for_model( + self.model, for_concrete_model=self.for_concrete_model).pk, "%s__in" % self.object_id_field_name: [obj.pk for obj in objs] }) @@ -247,8 +253,9 @@ class ReverseGenericRelatedObjectsDescriptor(object): "article.publications", the publications attribute is a ReverseGenericRelatedObjectsDescriptor instance. """ - def __init__(self, field): + def __init__(self, field, for_concrete_model=True): self.field = field + self.for_concrete_model = for_concrete_model def __get__(self, instance, instance_type=None): if instance is None: @@ -261,7 +268,8 @@ class ReverseGenericRelatedObjectsDescriptor(object): RelatedManager = create_generic_related_manager(superclass) qn = connection.ops.quote_name - content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(instance) + content_type = ContentType.objects.db_manager(instance._state.db).get_for_model( + instance, for_concrete_model=self.for_concrete_model) join_cols = self.field.get_joining_columns(reverse_join=True)[0] manager = RelatedManager( @@ -389,7 +397,8 @@ class BaseGenericInlineFormSet(BaseModelFormSet): if queryset is None: queryset = self.model._default_manager qs = queryset.filter(**{ - self.ct_field.name: ContentType.objects.get_for_model(self.instance), + self.ct_field.name: ContentType.objects.get_for_model( + self.instance, for_concrete_model=self.for_concrete_model), self.ct_fk_field.name: self.instance.pk, }) super(BaseGenericInlineFormSet, self).__init__( @@ -406,7 +415,8 @@ class BaseGenericInlineFormSet(BaseModelFormSet): def save_new(self, form, commit=True): kwargs = { - self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk, + self.ct_field.get_attname(): ContentType.objects.get_for_model( + self.instance, for_concrete_model=self.for_concrete_model).pk, self.ct_fk_field.get_attname(): self.instance.pk, } new_obj = self.model(**kwargs) @@ -418,7 +428,8 @@ def generic_inlineformset_factory(model, form=ModelForm, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, - formfield_callback=None, validate_max=False): + formfield_callback=None, validate_max=False, + for_concrete_model=True): """ Returns a ``GenericInlineFormSet`` for the given kwargs. @@ -444,6 +455,7 @@ def generic_inlineformset_factory(model, form=ModelForm, validate_max=validate_max) FormSet.ct_field = ct_field FormSet.ct_fk_field = fk_field + FormSet.for_concrete_model = for_concrete_model return FormSet class GenericInlineModelAdmin(InlineModelAdmin): diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 1bb0802442..de9c5dcbd6 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -303,6 +303,15 @@ model: :class:`~django.contrib.contenttypes.generic.GenericForeignKey` will look for. + .. attribute:: GenericForeignKey.for_concrete_model + + .. versionadded:: 1.6 + + If ``False``, the field will be able to reference proxy models. Default + is ``True``. This mirrors the ``for_concrete_model`` argument to + :meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_model`. + + .. admonition:: Primary key type compatibility The "object_id" field doesn't have to be the same type as the @@ -492,7 +501,7 @@ information. Subclasses of :class:`GenericInlineModelAdmin` with stacked and tabular layouts, respectively. -.. function:: generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False) +.. function:: generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True) Returns a ``GenericInlineFormSet`` using :func:`~django.forms.models.modelformset_factory`. @@ -502,3 +511,9 @@ information. are similar to those documented in :func:`~django.forms.models.modelformset_factory` and :func:`~django.forms.models.inlineformset_factory`. + + .. versionadded:: 1.6 + + The ``for_concrete_model`` argument corresponds to the + :class:`~django.contrib.contenttypes.generic.GenericForeignKey.for_concrete_model` + argument on ``GenericForeignKey``. diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 5ee454d4f8..8d48381c06 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -261,6 +261,11 @@ Minor features * :class:`~django.views.generic.base.View` and :class:`~django.views.generic.base.RedirectView` now support HTTP PATCH method. +* :class:`GenericForeignKey ` + now takes an optional ``for_concrete_model`` argument, which when set to + ``False`` allows the field to reference proxy models. The default is ``True`` + to retain the old behavior. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/generic_relations/models.py b/tests/generic_relations/models.py index 2acb982b9a..211df8aa3d 100644 --- a/tests/generic_relations/models.py +++ b/tests/generic_relations/models.py @@ -102,3 +102,22 @@ class Rock(Mineral): class ManualPK(models.Model): id = models.IntegerField(primary_key=True) tags = generic.GenericRelation(TaggedItem) + + +class ForProxyModelModel(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + obj = generic.GenericForeignKey(for_concrete_model=False) + title = models.CharField(max_length=255, null=True) + +class ForConcreteModelModel(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + obj = generic.GenericForeignKey() + +class ConcreteRelatedModel(models.Model): + bases = generic.GenericRelation(ForProxyModelModel, for_concrete_model=False) + +class ProxyRelatedModel(ConcreteRelatedModel): + class Meta: + proxy = True diff --git a/tests/generic_relations/tests.py b/tests/generic_relations/tests.py index 3716dd7560..1ed4989df8 100644 --- a/tests/generic_relations/tests.py +++ b/tests/generic_relations/tests.py @@ -6,7 +6,9 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from .models import (TaggedItem, ValuableTaggedItem, Comparison, Animal, - Vegetable, Mineral, Gecko, Rock, ManualPK) + Vegetable, Mineral, Gecko, Rock, ManualPK, + ForProxyModelModel, ForConcreteModelModel, + ProxyRelatedModel, ConcreteRelatedModel) class GenericRelationsTests(TestCase): @@ -256,12 +258,120 @@ class TaggedItemForm(forms.ModelForm): widgets = {'tag': CustomWidget} class GenericInlineFormsetTest(TestCase): - """ - Regression for #14572: Using base forms with widgets - defined in Meta should not raise errors. - """ - def test_generic_inlineformset_factory(self): + """ + Regression for #14572: Using base forms with widgets + defined in Meta should not raise errors. + """ Formset = generic_inlineformset_factory(TaggedItem, TaggedItemForm) form = Formset().forms[0] self.assertIsInstance(form['tag'].field.widget, CustomWidget) + + def test_save_new_for_proxy(self): + Formset = generic_inlineformset_factory(ForProxyModelModel, + fields='__all__', for_concrete_model=False) + + instance = ProxyRelatedModel.objects.create() + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-title': 'foo', + } + + formset = Formset(data, instance=instance, prefix='form') + self.assertTrue(formset.is_valid()) + + new_obj, = formset.save() + self.assertEqual(new_obj.obj, instance) + + def test_save_new_for_concrete(self): + Formset = generic_inlineformset_factory(ForProxyModelModel, + fields='__all__', for_concrete_model=True) + + instance = ProxyRelatedModel.objects.create() + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-title': 'foo', + } + + formset = Formset(data, instance=instance, prefix='form') + self.assertTrue(formset.is_valid()) + + new_obj, = formset.save() + self.assertNotIsInstance(new_obj.obj, ProxyRelatedModel) + + +class ProxyRelatedModelTest(TestCase): + def test_default_behavior(self): + """ + The default for for_concrete_model should be True + """ + base = ForConcreteModelModel() + base.obj = rel = ProxyRelatedModel.objects.create() + base.save() + + base = ForConcreteModelModel.objects.get(pk=base.pk) + rel = ConcreteRelatedModel.objects.get(pk=rel.pk) + self.assertEqual(base.obj, rel) + + def test_works_normally(self): + """ + When for_concrete_model is False, we should still be able to get + an instance of the concrete class. + """ + base = ForProxyModelModel() + base.obj = rel = ConcreteRelatedModel.objects.create() + base.save() + + base = ForProxyModelModel.objects.get(pk=base.pk) + self.assertEqual(base.obj, rel) + + def test_proxy_is_returned(self): + """ + Instances of the proxy should be returned when + for_concrete_model is False. + """ + base = ForProxyModelModel() + base.obj = ProxyRelatedModel.objects.create() + base.save() + + base = ForProxyModelModel.objects.get(pk=base.pk) + self.assertIsInstance(base.obj, ProxyRelatedModel) + + def test_query(self): + base = ForProxyModelModel() + base.obj = rel = ConcreteRelatedModel.objects.create() + base.save() + + self.assertEqual(rel, ConcreteRelatedModel.objects.get(bases__id=base.id)) + + def test_query_proxy(self): + base = ForProxyModelModel() + base.obj = rel = ProxyRelatedModel.objects.create() + base.save() + + self.assertEqual(rel, ProxyRelatedModel.objects.get(bases__id=base.id)) + + def test_generic_relation(self): + base = ForProxyModelModel() + base.obj = ProxyRelatedModel.objects.create() + base.save() + + base = ForProxyModelModel.objects.get(pk=base.pk) + rel = ProxyRelatedModel.objects.get(pk=base.obj.pk) + self.assertEqual(base, rel.bases.get()) + + def test_generic_relation_set(self): + base = ForProxyModelModel() + base.obj = ConcreteRelatedModel.objects.create() + base.save() + newrel = ConcreteRelatedModel.objects.create() + + newrel.bases = [base] + newrel = ConcreteRelatedModel.objects.get(pk=newrel.pk) + self.assertEqual(base, newrel.bases.get()) -- cgit v1.3 From fb1d8134846b468a6b972d3af1a23d7ee381b149 Mon Sep 17 00:00:00 2001 From: Paul Tax Date: Fri, 24 May 2013 13:37:20 +0300 Subject: Link to active fork for ODBC backend It took me quite some time to find if and where the ODBC backend was maintained. I found (on djangoproject.com): http://code.google.com/p/django-pyodbc/ (last commit about 3 years ago) then: https://github.com/avidal/django-pyodbc avidal fork. then: https://github.com/aurorasoftware/django-pyodbc/ aurorasoftware version which has avidal improvements merged. Avidals version now links to https://github.com/aurorasoftware/django-pyodbc/ which is also the version installed through PIP. --- docs/ref/databases.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/ref') diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 7555acaaba..b189f4f5f0 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -803,5 +803,5 @@ the support channels provided by each 3rd party project. .. _IBM DB2: http://code.google.com/p/ibm-db/ .. _Microsoft SQL Server 2005: http://code.google.com/p/django-mssql/ .. _Firebird: http://code.google.com/p/django-firebird/ -.. _ODBC: http://code.google.com/p/django-pyodbc/ +.. _ODBC: https://github.com/aurorasoftware/django-pyodbc/ .. _ADSDB: http://code.google.com/p/adsdb-django/ -- cgit v1.3 From 81f454a32296ad27437eaf19acfcdad9a9c03fca Mon Sep 17 00:00:00 2001 From: Alasdair Nicol Date: Fri, 24 May 2013 14:36:17 +0100 Subject: Update link to jQuery Cookie plugin site --- docs/ref/contrib/csrf.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/ref') diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index 9e58548376..f8b3cf2646 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -120,7 +120,7 @@ Acquiring the token is straightforward: var csrftoken = getCookie('csrftoken'); The above code could be simplified by using the `jQuery cookie plugin -`_ to replace ``getCookie``: +`_ to replace ``getCookie``: .. code-block:: javascript -- cgit v1.3 From fbab3209fc6b32752a71d012add57cd440adfc94 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 24 May 2013 12:35:20 -0400 Subject: Fixed #20492 - Removed a broken link in GIS docs. --- docs/ref/contrib/gis/geoip.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/contrib/gis/geoip.txt b/docs/ref/contrib/gis/geoip.txt index 2444849a19..b6aca6b211 100644 --- a/docs/ref/contrib/gis/geoip.txt +++ b/docs/ref/contrib/gis/geoip.txt @@ -8,8 +8,7 @@ Geolocation with GeoIP :synopsis: High-level Python interface for MaxMind's GeoIP C library. The :class:`GeoIP` object is a ctypes wrapper for the -`MaxMind GeoIP C API`__. [#]_ This interface is a BSD-licensed alternative -to the GPL-licensed `Python GeoIP`__ interface provided by MaxMind. +`MaxMind GeoIP C API`__. [#]_ In order to perform IP-based geolocation, the :class:`GeoIP` object requires the GeoIP C libary and either the GeoIP `Country`__ or `City`__ @@ -20,7 +19,6 @@ you set :setting:`GEOIP_PATH` with in your settings. See the example and reference below for more details. __ http://www.maxmind.com/app/c -__ http://www.maxmind.com/app/python __ http://www.maxmind.com/app/country __ http://www.maxmind.com/app/city __ http://www.maxmind.com/download/geoip/database/ -- cgit v1.3 From d228c1192ed59ab0114d9eba82ac99df611652d2 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Wed, 15 May 2013 16:14:28 -0700 Subject: Fixed #19866 -- Added security logger and return 400 for SuspiciousOperation. SuspiciousOperations have been differentiated into subclasses, and are now logged to a 'django.security.*' logger. SuspiciousOperations that reach django.core.handlers.base.BaseHandler will now return a 400 instead of a 500. Thanks to tiwoc for the report, and Carl Meyer and Donald Stufft for review. --- django/conf/urls/__init__.py | 3 +- django/contrib/admin/exceptions.py | 6 ++++ django/contrib/admin/views/main.py | 3 +- django/contrib/auth/tests/test_views.py | 36 +++++++++++++--------- django/contrib/formtools/exceptions.py | 6 ++++ django/contrib/formtools/wizard/storage/cookie.py | 4 +-- django/contrib/sessions/backends/base.py | 14 +++++++-- django/contrib/sessions/backends/cached_db.py | 9 +++++- django/contrib/sessions/backends/db.py | 10 ++++-- django/contrib/sessions/backends/file.py | 12 ++++++-- django/contrib/sessions/exceptions.py | 11 +++++++ django/contrib/sessions/tests.py | 20 +++++++++--- django/core/exceptions.py | 34 +++++++++++++++----- django/core/files/storage.py | 4 +-- django/core/handlers/base.py | 20 ++++++++++-- django/core/urlresolvers.py | 3 ++ django/http/multipartparser.py | 4 +-- django/http/request.py | 4 +-- django/http/response.py | 4 +-- django/test/utils.py | 20 ++++++++++++ django/utils/log.py | 5 +++ django/views/defaults.py | 15 +++++++++ docs/ref/exceptions.txt | 21 +++++++++++-- docs/releases/1.6.txt | 7 +++++ docs/topics/http/views.txt | 22 +++++++++++++ docs/topics/logging.txt | 31 ++++++++++++++++++- tests/admin_views/tests.py | 34 ++++++++++---------- tests/handlers/tests.py | 9 ++++++ tests/handlers/urls.py | 1 + tests/handlers/views.py | 4 +++ tests/logging_tests/tests.py | 24 +++++++++++++-- tests/logging_tests/urls.py | 10 ++++++ tests/logging_tests/views.py | 11 +++++++ tests/test_client_regress/tests.py | 6 ++-- tests/test_client_regress/views.py | 7 +++-- tests/urlpatterns_reverse/tests.py | 4 ++- tests/urlpatterns_reverse/urls_error_handlers.py | 1 + .../urls_error_handlers_callables.py | 1 + 38 files changed, 363 insertions(+), 77 deletions(-) create mode 100644 django/contrib/admin/exceptions.py create mode 100644 django/contrib/formtools/exceptions.py create mode 100644 django/contrib/sessions/exceptions.py create mode 100644 tests/logging_tests/urls.py create mode 100644 tests/logging_tests/views.py (limited to 'docs/ref') diff --git a/django/conf/urls/__init__.py b/django/conf/urls/__init__.py index 04fb1dff59..c0340c0543 100644 --- a/django/conf/urls/__init__.py +++ b/django/conf/urls/__init__.py @@ -5,8 +5,9 @@ from django.utils.importlib import import_module from django.utils import six -__all__ = ['handler403', 'handler404', 'handler500', 'include', 'patterns', 'url'] +__all__ = ['handler400', 'handler403', 'handler404', 'handler500', 'include', 'patterns', 'url'] +handler400 = 'django.views.defaults.bad_request' handler403 = 'django.views.defaults.permission_denied' handler404 = 'django.views.defaults.page_not_found' handler500 = 'django.views.defaults.server_error' diff --git a/django/contrib/admin/exceptions.py b/django/contrib/admin/exceptions.py new file mode 100644 index 0000000000..2e094c6da1 --- /dev/null +++ b/django/contrib/admin/exceptions.py @@ -0,0 +1,6 @@ +from django.core.exceptions import SuspiciousOperation + + +class DisallowedModelAdminLookup(SuspiciousOperation): + """Invalid filter was passed to admin view via URL querystring""" + pass diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 21ac30b7b3..dbed21265c 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -14,6 +14,7 @@ from django.utils.translation import ugettext, ugettext_lazy from django.utils.http import urlencode from django.contrib.admin import FieldListFilter +from django.contrib.admin.exceptions import DisallowedModelAdminLookup from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.util import (quote, get_fields_from_path, lookup_needs_distinct, prepare_lookup_value) @@ -128,7 +129,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): lookup_params[force_str(key)] = value if not self.model_admin.lookup_allowed(key, value): - raise SuspiciousOperation("Filtering by %s not allowed" % key) + raise DisallowedModelAdminLookup("Filtering by %s not allowed" % key) filter_specs = [] if self.list_filter: diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py index fe1d7fb52f..94cad90b15 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -10,7 +10,6 @@ from django.conf import global_settings, settings from django.contrib.sites.models import Site, RequestSite from django.contrib.auth.models import User from django.core import mail -from django.core.exceptions import SuspiciousOperation from django.core.urlresolvers import reverse, NoReverseMatch from django.http import QueryDict, HttpRequest from django.utils.encoding import force_text @@ -18,7 +17,7 @@ from django.utils.html import escape from django.utils.http import urlquote from django.utils._os import upath from django.test import TestCase -from django.test.utils import override_settings +from django.test.utils import override_settings, patch_logger from django.middleware.csrf import CsrfViewMiddleware from django.contrib.sessions.middleware import SessionMiddleware @@ -155,23 +154,28 @@ class PasswordResetTest(AuthViewsTestCase): # produce a meaningful reset URL, we need to be certain that the # HTTP_HOST header isn't poisoned. This is done as a check when get_host() # is invoked, but we check here as a practical consequence. - with self.assertRaises(SuspiciousOperation): - self.client.post('/password_reset/', - {'email': 'staffmember@example.com'}, - HTTP_HOST='www.example:dr.frankenstein@evil.tld' - ) - self.assertEqual(len(mail.outbox), 0) + with patch_logger('django.security.DisallowedHost', 'error') as logger_calls: + response = self.client.post('/password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='www.example:dr.frankenstein@evil.tld' + ) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(mail.outbox), 0) + self.assertEqual(len(logger_calls), 1) # Skip any 500 handler action (like sending more mail...) @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True) def test_poisoned_http_host_admin_site(self): "Poisoned HTTP_HOST headers can't be used for reset emails on admin views" - with self.assertRaises(SuspiciousOperation): - self.client.post('/admin_password_reset/', - {'email': 'staffmember@example.com'}, - HTTP_HOST='www.example:dr.frankenstein@evil.tld' - ) - self.assertEqual(len(mail.outbox), 0) + with patch_logger('django.security.DisallowedHost', 'error') as logger_calls: + response = self.client.post('/admin_password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='www.example:dr.frankenstein@evil.tld' + ) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(mail.outbox), 0) + self.assertEqual(len(logger_calls), 1) + def _test_confirm_start(self): # Start by creating the email @@ -678,5 +682,7 @@ class ChangelistTests(AuthViewsTestCase): self.login() # A lookup that tries to filter on password isn't OK - with self.assertRaises(SuspiciousOperation): + with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls: response = self.client.get('/admin/auth/user/?password__startswith=sha1$') + self.assertEqual(response.status_code, 400) + self.assertEqual(len(logger_calls), 1) diff --git a/django/contrib/formtools/exceptions.py b/django/contrib/formtools/exceptions.py new file mode 100644 index 0000000000..f07ac9f745 --- /dev/null +++ b/django/contrib/formtools/exceptions.py @@ -0,0 +1,6 @@ +from django.core.exceptions import SuspiciousOperation + + +class WizardViewCookieModified(SuspiciousOperation): + """Signature of cookie modified""" + pass diff --git a/django/contrib/formtools/wizard/storage/cookie.py b/django/contrib/formtools/wizard/storage/cookie.py index e80361042f..9bf6503f18 100644 --- a/django/contrib/formtools/wizard/storage/cookie.py +++ b/django/contrib/formtools/wizard/storage/cookie.py @@ -1,8 +1,8 @@ import json -from django.core.exceptions import SuspiciousOperation from django.core.signing import BadSignature +from django.contrib.formtools.exceptions import WizardViewCookieModified from django.contrib.formtools.wizard import storage @@ -21,7 +21,7 @@ class CookieStorage(storage.BaseStorage): except KeyError: data = None except BadSignature: - raise SuspiciousOperation('WizardView cookie manipulated') + raise WizardViewCookieModified('WizardView cookie manipulated') if data is None: return None return json.loads(data, cls=json.JSONDecoder) diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py index f79a264500..759d7ac7ad 100644 --- a/django/contrib/sessions/backends/base.py +++ b/django/contrib/sessions/backends/base.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals import base64 from datetime import datetime, timedelta +import logging + try: from django.utils.six.moves import cPickle as pickle except ImportError: @@ -14,7 +16,9 @@ from django.utils.crypto import constant_time_compare from django.utils.crypto import get_random_string from django.utils.crypto import salted_hmac from django.utils import timezone -from django.utils.encoding import force_bytes +from django.utils.encoding import force_bytes, force_text + +from django.contrib.sessions.exceptions import SuspiciousSession # session_key should not be case sensitive because some backends can store it # on case insensitive file systems. @@ -94,12 +98,16 @@ class SessionBase(object): hash, pickled = encoded_data.split(b':', 1) expected_hash = self._hash(pickled) if not constant_time_compare(hash.decode(), expected_hash): - raise SuspiciousOperation("Session data corrupted") + raise SuspiciousSession("Session data corrupted") else: return pickle.loads(pickled) - except Exception: + except Exception as e: # ValueError, SuspiciousOperation, unpickling exceptions. If any of # these happen, just return an empty dictionary (an empty session). + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) return {} def update(self, dict_): diff --git a/django/contrib/sessions/backends/cached_db.py b/django/contrib/sessions/backends/cached_db.py index 31c6fbfce3..be22c1f97a 100644 --- a/django/contrib/sessions/backends/cached_db.py +++ b/django/contrib/sessions/backends/cached_db.py @@ -2,10 +2,13 @@ Cached, database-backed sessions. """ +import logging + from django.contrib.sessions.backends.db import SessionStore as DBStore from django.core.cache import cache from django.core.exceptions import SuspiciousOperation from django.utils import timezone +from django.utils.encoding import force_text KEY_PREFIX = "django.contrib.sessions.cached_db" @@ -41,7 +44,11 @@ class SessionStore(DBStore): data = self.decode(s.session_data) cache.set(self.cache_key, data, self.get_expiry_age(expiry=s.expire_date)) - except (Session.DoesNotExist, SuspiciousOperation): + except (Session.DoesNotExist, SuspiciousOperation) as e: + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) self.create() data = {} return data diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py index 30da0b7a10..206fca2700 100644 --- a/django/contrib/sessions/backends/db.py +++ b/django/contrib/sessions/backends/db.py @@ -1,8 +1,10 @@ +import logging + from django.contrib.sessions.backends.base import SessionBase, CreateError from django.core.exceptions import SuspiciousOperation from django.db import IntegrityError, transaction, router from django.utils import timezone - +from django.utils.encoding import force_text class SessionStore(SessionBase): """ @@ -18,7 +20,11 @@ class SessionStore(SessionBase): expire_date__gt=timezone.now() ) return self.decode(s.session_data) - except (Session.DoesNotExist, SuspiciousOperation): + except (Session.DoesNotExist, SuspiciousOperation) as e: + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) self.create() return {} diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py index 3c3408e9a8..f47aa2d867 100644 --- a/django/contrib/sessions/backends/file.py +++ b/django/contrib/sessions/backends/file.py @@ -1,5 +1,6 @@ import datetime import errno +import logging import os import shutil import tempfile @@ -8,6 +9,9 @@ from django.conf import settings from django.contrib.sessions.backends.base import SessionBase, CreateError, VALID_KEY_CHARS from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured from django.utils import timezone +from django.utils.encoding import force_text + +from django.contrib.sessions.exceptions import InvalidSessionKey class SessionStore(SessionBase): """ @@ -48,7 +52,7 @@ class SessionStore(SessionBase): # should always be md5s, so they should never contain directory # components. if not set(session_key).issubset(set(VALID_KEY_CHARS)): - raise SuspiciousOperation( + raise InvalidSessionKey( "Invalid characters in session key") return os.path.join(self.storage_path, self.file_prefix + session_key) @@ -75,7 +79,11 @@ class SessionStore(SessionBase): if file_data: try: session_data = self.decode(file_data) - except (EOFError, SuspiciousOperation): + except (EOFError, SuspiciousOperation) as e: + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) self.create() # Remove expired sessions. diff --git a/django/contrib/sessions/exceptions.py b/django/contrib/sessions/exceptions.py new file mode 100644 index 0000000000..4f4dc6b048 --- /dev/null +++ b/django/contrib/sessions/exceptions.py @@ -0,0 +1,11 @@ +from django.core.exceptions import SuspiciousOperation + + +class InvalidSessionKey(SuspiciousOperation): + """Invalid characters in session key""" + pass + + +class SuspiciousSession(SuspiciousOperation): + """The session may be tampered with""" + pass diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index 1a7286e77e..cd8191a6a4 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -1,3 +1,4 @@ +import base64 from datetime import timedelta import os import shutil @@ -15,14 +16,16 @@ from django.contrib.sessions.models import Session from django.contrib.sessions.middleware import SessionMiddleware from django.core.cache import get_cache from django.core import management -from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation +from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse from django.test import TestCase, RequestFactory -from django.test.utils import override_settings +from django.test.utils import override_settings, patch_logger from django.utils import six from django.utils import timezone from django.utils import unittest +from django.contrib.sessions.exceptions import InvalidSessionKey + class SessionTestsMixin(object): # This does not inherit from TestCase to avoid any tests being run with this @@ -272,6 +275,15 @@ class SessionTestsMixin(object): encoded = self.session.encode(data) self.assertEqual(self.session.decode(encoded), data) + def test_decode_failure_logged_to_security(self): + bad_encode = base64.b64encode(b'flaskdj:alkdjf') + with patch_logger('django.security.SuspiciousSession', 'warning') as calls: + self.assertEqual({}, self.session.decode(bad_encode)) + # check that the failed decode is logged + self.assertEqual(len(calls), 1) + self.assertTrue('corrupted' in calls[0]) + + def test_actual_expiry(self): # Regression test for #19200 old_session_key = None @@ -411,12 +423,12 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase): # This is tested directly on _key_to_file, as load() will swallow # a SuspiciousOperation in the same way as an IOError - by creating # a new session, making it unclear whether the slashes were detected. - self.assertRaises(SuspiciousOperation, + self.assertRaises(InvalidSessionKey, self.backend()._key_to_file, "a\\b\\c") def test_invalid_key_forwardslash(self): # Ensure we don't allow directory-traversal - self.assertRaises(SuspiciousOperation, + self.assertRaises(InvalidSessionKey, self.backend()._key_to_file, "a/b/c") @override_settings(SESSION_ENGINE="django.contrib.sessions.backends.file") diff --git a/django/core/exceptions.py b/django/core/exceptions.py index 233af40f88..2c79736e33 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -1,6 +1,7 @@ """ Global Django exception and warning classes. """ +import logging from functools import reduce @@ -9,37 +10,56 @@ class DjangoRuntimeWarning(RuntimeWarning): class ObjectDoesNotExist(Exception): - "The requested object does not exist" + """The requested object does not exist""" silent_variable_failure = True class MultipleObjectsReturned(Exception): - "The query returned multiple objects when only one was expected." + """The query returned multiple objects when only one was expected.""" pass class SuspiciousOperation(Exception): - "The user did something suspicious" + """The user did something suspicious""" + + +class SuspiciousMultipartForm(SuspiciousOperation): + """Suspect MIME request in multipart form data""" + pass + + +class SuspiciousFileOperation(SuspiciousOperation): + """A Suspicious filesystem operation was attempted""" + pass + + +class DisallowedHost(SuspiciousOperation): + """HTTP_HOST header contains invalid value""" + pass + + +class DisallowedRedirect(SuspiciousOperation): + """Redirect to scheme not in allowed list""" pass class PermissionDenied(Exception): - "The user did not have permission to do that" + """The user did not have permission to do that""" pass class ViewDoesNotExist(Exception): - "The requested view does not exist" + """The requested view does not exist""" pass class MiddlewareNotUsed(Exception): - "This middleware is not used in this server configuration" + """This middleware is not used in this server configuration""" pass class ImproperlyConfigured(Exception): - "Django is somehow improperly configured" + """Django is somehow improperly configured""" pass diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 18d15e1ab6..977b6a68a8 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -8,7 +8,7 @@ import itertools from datetime import datetime from django.conf import settings -from django.core.exceptions import SuspiciousOperation +from django.core.exceptions import SuspiciousFileOperation from django.core.files import locks, File from django.core.files.move import file_move_safe from django.utils.encoding import force_text, filepath_to_uri @@ -260,7 +260,7 @@ class FileSystemStorage(Storage): try: path = safe_join(self.location, name) except ValueError: - raise SuspiciousOperation("Attempted access to '%s' denied." % name) + raise SuspiciousFileOperation("Attempted access to '%s' denied." % name) return os.path.normpath(path) def size(self, name): diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 555fd98fc6..59118656c6 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -8,7 +8,7 @@ from django import http from django.conf import settings from django.core import urlresolvers from django.core import signals -from django.core.exceptions import MiddlewareNotUsed, PermissionDenied +from django.core.exceptions import MiddlewareNotUsed, PermissionDenied, SuspiciousOperation from django.db import connections, transaction from django.utils.encoding import force_text from django.utils.module_loading import import_by_path @@ -170,11 +170,27 @@ class BaseHandler(object): response = self.handle_uncaught_exception(request, resolver, sys.exc_info()) + except SuspiciousOperation as e: + # The request logger receives events for any problematic request + # The security logger receives events for all SuspiciousOperations + security_logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + security_logger.error(force_text(e)) + + try: + callback, param_dict = resolver.resolve400() + response = callback(request, **param_dict) + except: + signals.got_request_exception.send( + sender=self.__class__, request=request) + response = self.handle_uncaught_exception(request, + resolver, sys.exc_info()) + except SystemExit: # Allow sys.exit() to actually exit. See tickets #1023 and #4701 raise - except: # Handle everything else, including SuspiciousOperation, etc. + except: # Handle everything else. # Get the exception info now, in case another exception is thrown later. signals.got_request_exception.send(sender=self.__class__, request=request) response = self.handle_uncaught_exception(request, resolver, sys.exc_info()) diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index c3d93bb247..5f314ff490 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -360,6 +360,9 @@ class RegexURLResolver(LocaleRegexProvider): callback = getattr(urls, 'handler%s' % view_type) return get_callable(callback), {} + def resolve400(self): + return self._resolve_special('400') + def resolve403(self): return self._resolve_special('403') diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index 26e10da1a2..eeb435fa57 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -11,7 +11,7 @@ import cgi import sys from django.conf import settings -from django.core.exceptions import SuspiciousOperation +from django.core.exceptions import SuspiciousMultipartForm from django.utils.datastructures import MultiValueDict from django.utils.encoding import force_text from django.utils import six @@ -370,7 +370,7 @@ class LazyStream(six.Iterator): if current_number == num_bytes]) if number_equal > 40: - raise SuspiciousOperation( + raise SuspiciousMultipartForm( "The multipart parser got stuck, which shouldn't happen with" " normal uploaded files. Check for malicious upload activity;" " if there is none, report this to the Django developers." diff --git a/django/http/request.py b/django/http/request.py index 749b9f2561..e6811aa6cc 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -14,7 +14,7 @@ except ImportError: from django.conf import settings from django.core import signing -from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured +from django.core.exceptions import DisallowedHost, ImproperlyConfigured from django.core.files import uploadhandler from django.http.multipartparser import MultiPartParser from django.utils import six @@ -72,7 +72,7 @@ class HttpRequest(object): msg = "Invalid HTTP_HOST header: %r." % host if domain: msg += "You may need to add %r to ALLOWED_HOSTS." % domain - raise SuspiciousOperation(msg) + raise DisallowedHost(msg) def get_full_path(self): # RFC 3986 requires query string arguments to be in the ASCII range. diff --git a/django/http/response.py b/django/http/response.py index 671fb1c573..9aa49b1d5f 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -12,7 +12,7 @@ except ImportError: from django.conf import settings from django.core import signals from django.core import signing -from django.core.exceptions import SuspiciousOperation +from django.core.exceptions import DisallowedRedirect from django.http.cookie import SimpleCookie from django.utils import six, timezone from django.utils.encoding import force_bytes, iri_to_uri @@ -452,7 +452,7 @@ class HttpResponseRedirectBase(HttpResponse): def __init__(self, redirect_to, *args, **kwargs): parsed = urlparse(redirect_to) if parsed.scheme and parsed.scheme not in self.allowed_schemes: - raise SuspiciousOperation("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) + raise DisallowedRedirect("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) super(HttpResponseRedirectBase, self).__init__(*args, **kwargs) self['Location'] = iri_to_uri(redirect_to) diff --git a/django/test/utils.py b/django/test/utils.py index fb9221d25c..d178c9b29c 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,3 +1,5 @@ +from contextlib import contextmanager +import logging import re import sys import warnings @@ -401,3 +403,21 @@ class IgnoreDeprecationWarningsMixin(object): class IgnorePendingDeprecationWarningsMixin(IgnoreDeprecationWarningsMixin): warning_class = PendingDeprecationWarning + + +@contextmanager +def patch_logger(logger_name, log_level): + """ + Context manager that takes a named logger and the logging level + and provides a simple mock-like list of messages received + """ + calls = [] + def replacement(msg): + calls.append(msg) + logger = logging.getLogger(logger_name) + orig = getattr(logger, log_level) + setattr(logger, log_level, replacement) + try: + yield calls + finally: + setattr(logger, log_level, orig) diff --git a/django/utils/log.py b/django/utils/log.py index a9b62caae1..2abf7701cb 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -63,6 +63,11 @@ DEFAULT_LOGGING = { 'level': 'ERROR', 'propagate': False, }, + 'django.security': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, 'py.warnings': { 'handlers': ['console'], }, diff --git a/django/views/defaults.py b/django/views/defaults.py index 89228c50c9..c8a62fc753 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -43,6 +43,21 @@ def server_error(request, template_name='500.html'): return http.HttpResponseServerError(template.render(Context({}))) +@requires_csrf_token +def bad_request(request, template_name='400.html'): + """ + 400 error handler. + + Templates: :template:`400.html` + Context: None + """ + try: + template = loader.get_template(template_name) + except TemplateDoesNotExist: + return http.HttpResponseBadRequest('

            Bad Request (400)

            ') + return http.HttpResponseBadRequest(template.render(Context({}))) + + # This can be called when CsrfViewMiddleware.process_view has not run, # therefore need @requires_csrf_token in case the template needs # {% csrf_token %}. diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt index f9a1715180..6a5e6f49d5 100644 --- a/docs/ref/exceptions.txt +++ b/docs/ref/exceptions.txt @@ -44,9 +44,24 @@ SuspiciousOperation ------------------- .. exception:: SuspiciousOperation - The :exc:`SuspiciousOperation` exception is raised when a user has performed - an operation that should be considered suspicious from a security perspective, - such as tampering with a session cookie. + The :exc:`SuspiciousOperation` exception is raised when a user has + performed an operation that should be considered suspicious from a security + perspective, such as tampering with a session cookie. Subclasses of + SuspiciousOperation include: + + * DisallowedHost + * DisallowedModelAdminLookup + * DisallowedRedirect + * InvalidSessionKey + * SuspiciousFileOperation + * SuspiciousMultipartForm + * SuspiciousSession + * WizardViewCookieModified + + If a ``SuspiciousOperation`` exception reaches the WSGI handler level it is + logged at the ``Error`` level and results in + a :class:`~django.http.HttpResponseBadRequest`. See the :doc:`logging + documentation ` for more information. PermissionDenied ---------------- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 35488b4110..825c5dc28d 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -270,6 +270,13 @@ Minor features stores active language in session if it is not present there. This prevents loss of language settings after session flush, e.g. logout. +* :exc:`~django.core.exceptions.SuspiciousOperation` has been differentiated + into a number of subclasses, and each will log to a matching named logger + under the ``django.security`` logging hierarchy. Along with this change, + a ``handler400`` mechanism and default view are used whenever + a ``SuspiciousOperation`` reaches the WSGI handler to return an + ``HttpResponseBadRequest``. + Backwards incompatible changes in 1.6 ===================================== diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index 2ccedec2f7..5c27c9c958 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -231,3 +231,25 @@ same way you can for the 404 and 500 views by specifying a ``handler403`` in your URLconf:: handler403 = 'mysite.views.my_custom_permission_denied_view' + +.. _http_bad_request_view: + +The 400 (bad request) view +-------------------------- + +When a :exc:`~django.core.exceptions.SuspiciousOperation` is raised in Django, +the it may be handled by a component of Django (for example resetting the +session data). If not specifically handled, Django will consider the current +request a 'bad request' instead of a server error. + +The view ``django.views.defaults.bad_request``, is otherwise very similar to +the ``server_error`` view, but returns with the status code 400 indicating that +the error condition was the result of a client operation. + +Like the ``server_error`` view, the default ``bad_request`` should suffice for +99% of Web applications, but if you want to override the view, you can specify +``handler400`` in your URLconf, like so:: + + handler400 = 'mysite.views.my_custom_bad_request_view' + +``bad_request`` views are also only used when :setting:`DEBUG` is ``False``. diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index a31dc01cc5..a88201ad47 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -394,7 +394,7 @@ requirements of logging in Web server environment. Loggers ------- -Django provides three built-in loggers. +Django provides four built-in loggers. ``django`` ~~~~~~~~~~ @@ -434,6 +434,35 @@ For performance reasons, SQL logging is only enabled when ``settings.DEBUG`` is set to ``True``, regardless of the logging level or handlers that are installed. +``django.security.*`` +~~~~~~~~~~~~~~~~~~~~~~ + +The security loggers will receive messages on any occurrence of +:exc:`~django.core.exceptions.SuspiciousOperation`. There is a sub-logger for +each sub-type of SuspiciousOperation. The level of the log event depends on +where the exception is handled. Most occurrences are logged as a warning, while +any ``SuspiciousOperation`` that reaches the WSGI handler will be logged as an +error. For example, when an HTTP ``Host`` header is included in a request from +a client that does not match :setting:`ALLOWED_HOSTS`, Django will return a 400 +response, and an error message will be logged to the +``django.security.DisallowedHost`` logger. + +Only the parent ``django.security`` logger is configured by default, and all +child loggers will propagate to the parent logger. The ``django.security`` +logger is configured the same as the ``django.request`` logger, and any error +events will be mailed to admins. Requests resulting in a 400 response due to +a ``SuspiciousOperation`` will not be logged to the ``django.request`` logger, +but only to the ``django.security`` logger. + +To silence a particular type of SuspiciousOperation, you can override that +specific logger following this example:: + + 'loggers': { + 'django.security.DisallowedHost': { + 'handlers': ['null'], + 'propagate': False, + }, + Handlers -------- diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index c0f6533cb2..3485ea353b 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -11,7 +11,6 @@ except ImportError: # Python 2 from django.conf import settings, global_settings from django.core import mail -from django.core.exceptions import SuspiciousOperation from django.core.files import temp as tempfile from django.core.urlresolvers import reverse # Register auth models with the admin. @@ -30,6 +29,7 @@ from django.db import connection from django.forms.util import ErrorList from django.template.response import TemplateResponse from django.test import TestCase +from django.test.utils import patch_logger from django.utils import formats, translation, unittest from django.utils.cache import get_max_age from django.utils.encoding import iri_to_uri, force_bytes @@ -543,20 +543,21 @@ class AdminViewBasicTest(TestCase): self.assertContains(response, '%Y-%m-%d %H:%M:%S') def test_disallowed_filtering(self): - self.assertRaises(SuspiciousOperation, - self.client.get, "/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy" - ) + with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as calls: + response = self.client.get("/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy") + self.assertEqual(response.status_code, 400) + self.assertEqual(len(calls), 1) - try: - self.client.get("/test_admin/admin/admin_views/thing/?color__value__startswith=red") - self.client.get("/test_admin/admin/admin_views/thing/?color__value=red") - except SuspiciousOperation: - self.fail("Filters are allowed if explicitly included in list_filter") + # Filters are allowed if explicitly included in list_filter + response = self.client.get("/test_admin/admin/admin_views/thing/?color__value__startswith=red") + self.assertEqual(response.status_code, 200) + response = self.client.get("/test_admin/admin/admin_views/thing/?color__value=red") + self.assertEqual(response.status_code, 200) - try: - self.client.get("/test_admin/admin/admin_views/person/?age__gt=30") - except SuspiciousOperation: - self.fail("Filters should be allowed if they involve a local field without the need to whitelist them in list_filter or date_hierarchy.") + # Filters should be allowed if they involve a local field without the + # need to whitelist them in list_filter or date_hierarchy. + response = self.client.get("/test_admin/admin/admin_views/person/?age__gt=30") + self.assertEqual(response.status_code, 200) e1 = Employee.objects.create(name='Anonymous', gender=1, age=22, alive=True, code='123') e2 = Employee.objects.create(name='Visitor', gender=2, age=19, alive=True, code='124') @@ -574,10 +575,9 @@ class AdminViewBasicTest(TestCase): ForeignKey 'limit_choices_to' should be allowed, otherwise raw_id_fields can break. """ - try: - self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27") - except SuspiciousOperation: - self.fail("Filters should be allowed if they are defined on a ForeignKey pointing to this model") + # Filters should be allowed if they are defined on a ForeignKey pointing to this model + response = self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27") + self.assertEqual(response.status_code, 200) def test_hide_change_password(self): """ diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index 3680eecdd2..397b63647a 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -61,6 +61,7 @@ class TransactionsPerRequestTests(TransactionTestCase): connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests self.assertContains(response, 'False') + class SignalsTests(TestCase): urls = 'handlers.urls' @@ -89,3 +90,11 @@ class SignalsTests(TestCase): self.assertEqual(self.signals, ['started']) self.assertEqual(b''.join(response.streaming_content), b"streaming content") self.assertEqual(self.signals, ['started', 'finished']) + + +class HandlerSuspiciousOpsTest(TestCase): + urls = 'handlers.urls' + + def test_suspiciousop_in_view_returns_400(self): + response = self.client.get('/suspicious/') + self.assertEqual(response.status_code, 400) diff --git a/tests/handlers/urls.py b/tests/handlers/urls.py index 29858055ab..eaf764b00b 100644 --- a/tests/handlers/urls.py +++ b/tests/handlers/urls.py @@ -9,4 +9,5 @@ urlpatterns = patterns('', url(r'^streaming/$', views.streaming), url(r'^in_transaction/$', views.in_transaction), url(r'^not_in_transaction/$', views.not_in_transaction), + url(r'^suspicious/$', views.suspicious), ) diff --git a/tests/handlers/views.py b/tests/handlers/views.py index 9cc86ae6f3..1b75b27043 100644 --- a/tests/handlers/views.py +++ b/tests/handlers/views.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from django.core.exceptions import SuspiciousOperation from django.db import connection, transaction from django.http import HttpResponse, StreamingHttpResponse @@ -15,3 +16,6 @@ def in_transaction(request): @transaction.non_atomic_requests def not_in_transaction(request): return HttpResponse(str(connection.in_atomic_block)) + +def suspicious(request): + raise SuspiciousOperation('dubious') diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py index 8e5445cd42..0c2d269464 100644 --- a/tests/logging_tests/tests.py +++ b/tests/logging_tests/tests.py @@ -8,9 +8,10 @@ import warnings from django.conf import LazySettings from django.core import mail from django.test import TestCase, RequestFactory -from django.test.utils import override_settings +from django.test.utils import override_settings, patch_logger from django.utils.encoding import force_text -from django.utils.log import CallbackFilter, RequireDebugFalse, RequireDebugTrue +from django.utils.log import (CallbackFilter, RequireDebugFalse, + RequireDebugTrue) from django.utils.six import StringIO from django.utils.unittest import skipUnless @@ -354,3 +355,22 @@ class SettingsConfigureLogging(TestCase): settings.configure( LOGGING_CONFIG='logging_tests.tests.dictConfig') self.assertTrue(dictConfig.called) + + +class SecurityLoggerTest(TestCase): + + urls = 'logging_tests.urls' + + def test_suspicious_operation_creates_log_message(self): + with self.settings(DEBUG=True): + with patch_logger('django.security.SuspiciousOperation', 'error') as calls: + response = self.client.get('/suspicious/') + self.assertEqual(len(calls), 1) + self.assertEqual(calls[0], 'dubious') + + def test_suspicious_operation_uses_sublogger(self): + with self.settings(DEBUG=True): + with patch_logger('django.security.DisallowedHost', 'error') as calls: + response = self.client.get('/suspicious_spec/') + self.assertEqual(len(calls), 1) + self.assertEqual(calls[0], 'dubious') diff --git a/tests/logging_tests/urls.py b/tests/logging_tests/urls.py new file mode 100644 index 0000000000..c738bd565c --- /dev/null +++ b/tests/logging_tests/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + +from django.conf.urls import patterns, url + +from . import views + +urlpatterns = patterns('', + url(r'^suspicious/$', views.suspicious), + url(r'^suspicious_spec/$', views.suspicious_spec), +) diff --git a/tests/logging_tests/views.py b/tests/logging_tests/views.py new file mode 100644 index 0000000000..c685bcc005 --- /dev/null +++ b/tests/logging_tests/views.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django.core.exceptions import SuspiciousOperation, DisallowedHost + + +def suspicious(request): + raise SuspiciousOperation('dubious') + + +def suspicious_spec(request): + raise DisallowedHost('dubious') diff --git a/tests/test_client_regress/tests.py b/tests/test_client_regress/tests.py index 2582b210c4..6a58ef6344 100644 --- a/tests/test_client_regress/tests.py +++ b/tests/test_client_regress/tests.py @@ -7,7 +7,6 @@ from __future__ import unicode_literals import os from django.conf import settings -from django.core.exceptions import SuspiciousOperation from django.core.urlresolvers import reverse from django.template import (TemplateDoesNotExist, TemplateSyntaxError, Context, Template, loader) @@ -20,6 +19,7 @@ from django.utils._os import upath from django.utils.translation import ugettext_lazy from django.http import HttpResponse +from .views import CustomTestException @override_settings( TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'templates'),) @@ -619,7 +619,7 @@ class ExceptionTests(TestCase): try: response = self.client.get("/test_client_regress/staff_only/") self.fail("General users should not be able to visit this page") - except SuspiciousOperation: + except CustomTestException: pass # At this point, an exception has been raised, and should be cleared. @@ -629,7 +629,7 @@ class ExceptionTests(TestCase): self.assertTrue(login, 'Could not log in') try: self.client.get("/test_client_regress/staff_only/") - except SuspiciousOperation: + except CustomTestException: self.fail("Staff should be able to visit this page") diff --git a/tests/test_client_regress/views.py b/tests/test_client_regress/views.py index 7e86ffd8ca..71e5b526e5 100644 --- a/tests/test_client_regress/views.py +++ b/tests/test_client_regress/views.py @@ -3,12 +3,15 @@ import json from django.conf import settings from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect -from django.core.exceptions import SuspiciousOperation from django.shortcuts import render_to_response from django.core.serializers.json import DjangoJSONEncoder from django.test.client import CONTENT_TYPE_RE from django.template import RequestContext + +class CustomTestException(Exception): + pass + def no_template_view(request): "A simple view that expects a GET request, and returns a rendered template" return HttpResponse("No template used. Sample content: twice once twice. Content ends.") @@ -18,7 +21,7 @@ def staff_only_view(request): if request.user.is_staff: return HttpResponse('') else: - raise SuspiciousOperation() + raise CustomTestException() def get_view(request): "A simple login protected view" diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index d5d9b3a709..f54c796d30 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/tests/urlpatterns_reverse/tests.py @@ -516,7 +516,7 @@ class RequestURLconfTests(TestCase): b''.join(self.client.get('/second_test/')) class ErrorHandlerResolutionTests(TestCase): - """Tests for handler404 and handler500""" + """Tests for handler400, handler404 and handler500""" def setUp(self): from django.core.urlresolvers import RegexURLResolver @@ -528,12 +528,14 @@ class ErrorHandlerResolutionTests(TestCase): def test_named_handlers(self): from .views import empty_view handler = (empty_view, {}) + self.assertEqual(self.resolver.resolve400(), handler) self.assertEqual(self.resolver.resolve404(), handler) self.assertEqual(self.resolver.resolve500(), handler) def test_callable_handers(self): from .views import empty_view handler = (empty_view, {}) + self.assertEqual(self.callable_resolver.resolve400(), handler) self.assertEqual(self.callable_resolver.resolve404(), handler) self.assertEqual(self.callable_resolver.resolve500(), handler) diff --git a/tests/urlpatterns_reverse/urls_error_handlers.py b/tests/urlpatterns_reverse/urls_error_handlers.py index be4f42afbc..7146fdf43c 100644 --- a/tests/urlpatterns_reverse/urls_error_handlers.py +++ b/tests/urlpatterns_reverse/urls_error_handlers.py @@ -4,5 +4,6 @@ from django.conf.urls import patterns urlpatterns = patterns('') +handler400 = 'urlpatterns_reverse.views.empty_view' handler404 = 'urlpatterns_reverse.views.empty_view' handler500 = 'urlpatterns_reverse.views.empty_view' diff --git a/tests/urlpatterns_reverse/urls_error_handlers_callables.py b/tests/urlpatterns_reverse/urls_error_handlers_callables.py index fe2d3137e9..befeccaf45 100644 --- a/tests/urlpatterns_reverse/urls_error_handlers_callables.py +++ b/tests/urlpatterns_reverse/urls_error_handlers_callables.py @@ -9,5 +9,6 @@ from .views import empty_view urlpatterns = patterns('') +handler400 = empty_view handler404 = empty_view handler500 = empty_view -- cgit v1.3 From 08726cb0136c9de88568e7d6b77249b817094660 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Mon, 27 May 2013 11:34:14 +0200 Subject: Fix #20505: Typo in BinaryField documentation. --- docs/ref/models/fields.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs/ref') diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index c5f2609bab..f61ebe6684 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -368,7 +368,7 @@ to filter a queryset on a ``BinaryField`` value. Although you might think about storing files in the database, consider that it is bad design in 99% of the cases. This field is *not* a replacement for - proper :doc`static files ` handling. + proper :doc:`static files ` handling. ``BooleanField`` ---------------- -- cgit v1.3 From 90af278203963e3e3f96e443971cd38a2dad34e4 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 24 May 2013 16:36:09 -0400 Subject: Fixed #16137 - Removed kwargs requirement for QuerySet.get_or_create Thanks wilfred@, poirier, and charettes for work on the patch. --- django/db/models/query.py | 2 -- docs/ref/models/querysets.txt | 8 ++++++-- docs/releases/1.6.txt | 3 +++ tests/get_or_create/models.py | 5 +++++ tests/get_or_create/tests.py | 10 +++++++++- 5 files changed, 23 insertions(+), 5 deletions(-) (limited to 'docs/ref') diff --git a/django/db/models/query.py b/django/db/models/query.py index 71d432f7d5..5e8b3d995c 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -363,8 +363,6 @@ class QuerySet(object): Returns a tuple of (object, created), where created is a boolean specifying whether an object was created. """ - assert kwargs, \ - 'get_or_create() must be passed at least one keyword argument' defaults = kwargs.pop('defaults', {}) lookup = kwargs.copy() for f in self.model._meta.fields: diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 53359441ae..3fb5b3a498 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1343,8 +1343,12 @@ get_or_create .. method:: get_or_create(**kwargs) -A convenience method for looking up an object with the given kwargs, creating -one if necessary. +A convenience method for looking up an object with the given kwargs (may be +empty if your model has defaults for all fields), creating one if necessary. + +.. versionchanged:: 1.6 + + Older versions of Django required ``kwargs``. Returns a tuple of ``(object, created)``, where ``object`` is the retrieved or created object and ``created`` is a boolean specifying whether a new object was diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 73b0fb85eb..4a8f7c6ecf 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -280,6 +280,9 @@ Minor features * The :exc:`~django.core.exceptions.DoesNotExist` exception now includes a message indicating the name of the attribute used for the lookup. +* The :meth:`~django.db.models.query.QuerySet.get_or_create` method no longer + requires at least one keyword argument. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/get_or_create/models.py b/tests/get_or_create/models.py index 84c8fda968..1a85de2e74 100644 --- a/tests/get_or_create/models.py +++ b/tests/get_or_create/models.py @@ -21,6 +21,11 @@ class Person(models.Model): def __str__(self): return '%s %s' % (self.first_name, self.last_name) + +class DefaultPerson(models.Model): + first_name = models.CharField(max_length=100, default="Anonymous") + + class ManualPrimaryKeyTest(models.Model): id = models.IntegerField(primary_key=True) data = models.CharField(max_length=100) diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index 48f2a067c1..36c248b169 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -8,7 +8,7 @@ from django.db import IntegrityError, DatabaseError from django.utils.encoding import DjangoUnicodeDecodeError from django.test import TestCase, TransactionTestCase -from .models import Person, ManualPrimaryKeyTest, Profile, Tag, Thing +from .models import DefaultPerson, Person, ManualPrimaryKeyTest, Profile, Tag, Thing class GetOrCreateTests(TestCase): @@ -83,6 +83,14 @@ class GetOrCreateTests(TestCase): else: self.skipTest("This backend accepts broken utf-8.") + def test_get_or_create_empty(self): + # Regression test for #16137: get_or_create does not require kwargs. + try: + DefaultPerson.objects.get_or_create() + except AssertionError: + self.fail("If all the attributes on a model have defaults, we " + "shouldn't need to pass any arguments.") + class GetOrCreateTransactionTests(TransactionTestCase): -- cgit v1.3 From d321d1acf0fdf00247e78b9686be84c18b35b9d8 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 28 May 2013 09:56:14 -0400 Subject: Fixed #20228 - Documented unique_for_date and exclude behavior. Thanks Deepak Thukral for the patch. --- docs/ref/models/fields.txt | 7 ++++++- tests/model_forms/models.py | 12 +++++++++++- tests/model_forms/tests.py | 25 ++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index f61ebe6684..8146dfd341 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -293,7 +293,12 @@ records with the same ``title`` and ``pub_date``. Note that if you set this to point to a :class:`DateTimeField`, only the date portion of the field will be considered. -This is enforced by model validation but not at the database level. +This is enforced by :meth:`Model.validate_unique()` during model validation +but not at the database level. If any :attr:`~Field.unique_for_date` constraint +involves fields that are not part of a :class:`~django.forms.ModelForm` (for +example, if one of the fields is listed in ``exclude`` or has +:attr:`editable=False`), :meth:`Model.validate_unique()` will +skip validation for that particular constraint. ``unique_for_month`` -------------------- diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index 9c5e097106..610dc34001 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -207,7 +207,17 @@ class Post(models.Model): posted = models.DateField() def __str__(self): - return self.name + return self.title + +@python_2_unicode_compatible +class DateTimePost(models.Model): + title = models.CharField(max_length=50, unique_for_date='posted', blank=True) + slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) + subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) + posted = models.DateTimeField(editable=False) + + def __str__(self): + return self.title class DerivedPost(Post): pass diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index 5219804e0c..58dde13a8a 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -24,7 +24,7 @@ from .models import (Article, ArticleStatus, BetterAuthor, BigInt, DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle, ImprovedArticleWithParentLink, Inventory, Post, Price, Product, TextFile, AuthorProfile, Colour, ColourfulItem, - ArticleStatusNote, test_images) + ArticleStatusNote, DateTimePost, test_images) if test_images: from .models import ImageFile, OptionalImageFile @@ -76,6 +76,12 @@ class PostForm(forms.ModelForm): fields = '__all__' +class DateTimePostForm(forms.ModelForm): + class Meta: + model = DateTimePost + fields = '__all__' + + class DerivedPostForm(forms.ModelForm): class Meta: model = DerivedPost @@ -682,6 +688,23 @@ class UniqueTest(TestCase): self.assertEqual(len(form.errors), 1) self.assertEqual(form.errors['posted'], ['This field is required.']) + def test_unique_for_date_in_exclude(self): + """If the date for unique_for_* constraints is excluded from the + ModelForm (in this case 'posted' has editable=False, then the + constraint should be ignored.""" + p = DateTimePost.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", + posted=datetime.datetime(2008, 9, 3, 10, 10, 1)) + # 'title' has unique_for_date='posted' + form = DateTimePostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'}) + self.assertTrue(form.is_valid()) + # 'slug' has unique_for_year='posted' + form = DateTimePostForm({'slug': "Django 1.0", 'posted': '2008-01-01'}) + self.assertTrue(form.is_valid()) + # 'subtitle' has unique_for_month='posted' + form = DateTimePostForm({'subtitle': "Finally", 'posted': '2008-09-30'}) + self.assertTrue(form.is_valid()) + def test_inherited_unique_for_date(self): p = Post.objects.create(title="Django 1.0 is released", slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) -- cgit v1.3 From 8a6e040bfa58c69ba9760a67d7d2e714bffccfc1 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 28 May 2013 20:14:01 -0400 Subject: Fixed #20525 -- Added versionadded for clearsessions. Thanks wiml@. --- docs/ref/django-admin.txt | 2 ++ 1 file changed, 2 insertions(+) (limited to 'docs/ref') diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index e193a448d2..e21e3d2766 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1323,6 +1323,8 @@ clearsessions .. django-admin:: clearsessions +.. versionadded:: 1.5 + Can be run as a cron job or directly to clean out expired sessions. ``django.contrib.sitemaps`` -- cgit v1.3 From e6ff238431ebfdfe957ca05dea5f69d3750d7f22 Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Wed, 29 May 2013 16:33:51 -0600 Subject: Fixed regroup example. Chicago was missing. --- docs/ref/templates/builtins.txt | 2 ++ 1 file changed, 2 insertions(+) (limited to 'docs/ref') diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 287fd4f59e..24eda2ce2c 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -862,6 +862,8 @@ above would result in the following output: * New York: 20,000,000 * India * Calcutta: 15,000,000 +* USA + * Chicago: 7,000,000 * Japan * Tokyo: 33,000,000 -- cgit v1.3 From 5074c75a37f88726f3ae057999144545881d3cfc Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 30 May 2013 11:05:42 -0400 Subject: Fixed #16856 - Added a way to clear select_related. Thanks Carl for the suggestion and David Cramer for the patch. --- django/db/models/query.py | 6 +++++- docs/ref/models/querysets.txt | 7 +++++++ docs/releases/1.6.txt | 5 +++++ tests/select_related/tests.py | 4 ++++ 4 files changed, 21 insertions(+), 1 deletion(-) (limited to 'docs/ref') diff --git a/django/db/models/query.py b/django/db/models/query.py index 5e8b3d995c..b0ce25f5b5 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -645,6 +645,8 @@ class QuerySet(object): If fields are specified, they must be ForeignKey fields and only those related objects are included in the selection. + + If select_related(None) is called, the list is cleared. """ if 'depth' in kwargs: warnings.warn('The "depth" keyword argument has been deprecated.\n' @@ -654,7 +656,9 @@ class QuerySet(object): raise TypeError('Unexpected keyword arguments to select_related: %s' % (list(kwargs),)) obj = self._clone() - if fields: + if fields == (None,): + obj.query.select_related = False + elif fields: if depth: raise TypeError('Cannot pass both "depth" and fields to select_related()') obj.query.add_select_related(fields) diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 3fb5b3a498..2788143899 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -774,6 +774,13 @@ You can also refer to the reverse direction of a is defined. Instead of specifying the field name, use the :attr:`related_name ` for the field on the related object. +.. versionadded:: 1.6 + +If you need to clear the list of related fields added by past calls of +``select_related`` on a ``QuerySet``, you can pass ``None`` as a parameter:: + + >>> without_relations = queryset.select_related(None) + .. deprecated:: 1.5 The ``depth`` parameter to ``select_related()`` has been deprecated. You should replace it with the use of the ``(*fields)`` listing specific diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index efc1297265..c8e6044a06 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -287,6 +287,11 @@ Minor features helper for testing formset errors: :meth:`~django.test.SimpleTestCase.assertFormsetError`. +* The list of related fields added to a + :class:`~django.db.models.query.QuerySet` by + :meth:`~django.db.models.query.QuerySet.select_related` can be cleared using + ``select_related(None)``. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/select_related/tests.py b/tests/select_related/tests.py index 27d65fecb1..baa141d123 100644 --- a/tests/select_related/tests.py +++ b/tests/select_related/tests.py @@ -172,3 +172,7 @@ class SelectRelatedTests(TestCase): Species.objects.select_related, 'genus__family__order', depth=4 ) + + def test_none_clears_list(self): + queryset = Species.objects.select_related('genus').select_related(None) + self.assertEqual(queryset.query.select_related, False) -- cgit v1.3 From 36aecb12b850aeed173a8e524cacb3444f807785 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 30 May 2013 13:48:10 -0400 Subject: Fixed #19425 - Added InlineModelAdmin.get_extra hook. Thanks dave@ for the suggestion and Rohan Jain for the patch. --- django/contrib/admin/options.py | 6 +++++- docs/ref/contrib/admin/index.txt | 25 +++++++++++++++++++++++++ docs/releases/1.6.txt | 4 ++++ tests/admin_inlines/admin.py | 12 ++++++++++++ tests/admin_inlines/models.py | 6 ++++++ tests/admin_inlines/tests.py | 14 +++++++++++++- 6 files changed, 65 insertions(+), 2 deletions(-) (limited to 'docs/ref') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 6202646988..a0a82f32ff 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1512,6 +1512,10 @@ class InlineModelAdmin(BaseModelAdmin): js.extend(['SelectBox.js', 'SelectFilter2.js']) return forms.Media(js=[static('admin/js/%s' % url) for url in js]) + def get_extra(self, request, obj=None, **kwargs): + """Hook for customizing the number of extra inline forms.""" + return self.extra + def get_formset(self, request, obj=None, **kwargs): """Returns a BaseInlineFormSet class for use in admin add/change views.""" if self.declared_fieldsets: @@ -1538,7 +1542,7 @@ class InlineModelAdmin(BaseModelAdmin): "fields": fields, "exclude": exclude, "formfield_callback": partial(self.formfield_for_dbfield, request=request), - "extra": self.extra, + "extra": self.get_extra(request, obj, **kwargs), "max_num": self.max_num, "can_delete": can_delete, } diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 0aec62f7b9..73ea74adc0 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1715,6 +1715,11 @@ The ``InlineModelAdmin`` class adds: The dynamic link will not appear if the number of currently displayed forms exceeds ``max_num``, or if the user does not have JavaScript enabled. + .. versionadded:: 1.6 + + :meth:`InlineModelAdmin.get_extra` also allows you to customize the number + of extra forms. + .. _ref-contrib-admin-inline-max-num: .. attribute:: InlineModelAdmin.max_num @@ -1762,6 +1767,26 @@ The ``InlineModelAdmin`` class adds: Returns a ``BaseInlineFormSet`` class for use in admin add/change views. See the example for :class:`ModelAdmin.get_formsets`. +.. method:: InlineModelAdmin.get_extra(self, request, obj=None, **kwargs) + + .. versionadded:: 1.6 + + Returns the number of extra inline forms to use. By default, returns the + :attr:`InlineModelAdmin.extra` attribute. + + Override this method to programmatically determine the number of extra + inline forms. For example, this may be based on the model instance + (passed as the keyword argument ``obj``):: + + class BinaryTreeAdmin(admin.TabularInline): + model = BinaryTree + + def get_extra(self, request, obj=None, **kwargs): + extra = 2 + if obj: + return extra - obj.binarytree_set.count() + return extra + Working with a model with two or more foreign keys to the same parent model --------------------------------------------------------------------------- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index c8e6044a06..2afa9a32cd 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -292,6 +292,10 @@ Minor features :meth:`~django.db.models.query.QuerySet.select_related` can be cleared using ``select_related(None)``. +* The :meth:`~django.contrib.admin.InlineModelAdmin.get_extra` method on + :class:`~django.contrib.admin.InlineModelAdmin` may be overridden to + customize the number of extra inline forms. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py index 44671d0ac4..2bc9dc5234 100644 --- a/tests/admin_inlines/admin.py +++ b/tests/admin_inlines/admin.py @@ -129,6 +129,17 @@ class ChildModel1Inline(admin.TabularInline): class ChildModel2Inline(admin.StackedInline): model = ChildModel2 +# admin for #19425 +class BinaryTreeAdmin(admin.TabularInline): + model = BinaryTree + + def get_extra(self, request, obj=None, **kwargs): + extra = 2 + if obj: + return extra - obj.binarytree_set.count() + + return extra + # admin for #19524 class SightingInline(admin.TabularInline): model = Sighting @@ -150,4 +161,5 @@ site.register(Author, AuthorAdmin) site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline, ReadOnlyInlineInline]) site.register(ProfileCollection, inlines=[ProfileInline]) site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2Inline]) +site.register(BinaryTree, inlines=[BinaryTreeAdmin]) site.register(ExtraTerrestrial, inlines=[SightingInline]) diff --git a/tests/admin_inlines/models.py b/tests/admin_inlines/models.py index 82c1c3f078..d4ba0ab6bc 100644 --- a/tests/admin_inlines/models.py +++ b/tests/admin_inlines/models.py @@ -183,6 +183,12 @@ class ChildModel2(models.Model): def get_absolute_url(self): return '/child_model2/' + +# Models for #19425 +class BinaryTree(models.Model): + name = models.CharField(max_length=100) + parent = models.ForeignKey('self', null=True, blank=True) + # Models for #19524 class LifeForm(models.Model): diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 714a2f1c61..23c7f95bfe 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -12,7 +12,7 @@ from .admin import InnerInline, TitleInline, site from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, - Sighting, Title, Novel, Chapter, FootNote) + Sighting, Title, Novel, Chapter, FootNote, BinaryTree) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -193,6 +193,18 @@ class TestInline(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(Sighting.objects.filter(et__name='Martian').count(), 1) + def test_custom_get_extra_form(self): + bt_head = BinaryTree.objects.create(name="Tree Head") + bt_child = BinaryTree.objects.create(name="First Child", parent=bt_head) + + # The total number of forms will remain the same in either case + total_forms_hidden = '' + response = self.client.get('/admin/admin_inlines/binarytree/add/') + self.assertContains(response, total_forms_hidden) + + response = self.client.get("/admin/admin_inlines/binarytree/%d/" % bt_head.id) + self.assertContains(response, total_forms_hidden) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class TestInlineMedia(TestCase): -- cgit v1.3 From ac90aee55cb0258253011ec6910c1e553e07377f Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 30 May 2013 20:38:44 -0300 Subject: Tweak caching decorators/utility functions xrefs. --- docs/ref/request-response.txt | 4 ++-- docs/topics/cache.txt | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 9ff97e87d0..578418b4ee 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -591,8 +591,8 @@ Note that unlike a dictionary, ``del`` doesn't raise ``KeyError`` if the header field doesn't exist. For setting the ``Cache-Control`` and ``Vary`` header fields, it is recommended -to use the :meth:`~django.utils.cache.patch_cache_control` and -:meth:`~django.utils.cache.patch_vary_headers` methods from +to use the :func:`~django.utils.cache.patch_cache_control` and +:func:`~django.utils.cache.patch_vary_headers` methods from :mod:`django.utils.cache`, since these fields can have multiple, comma-separated values. The "patch" methods ensure that other values, e.g. added by a middleware, are not removed. diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 46911a593f..2352770bad 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -997,8 +997,8 @@ produces different content based on some difference in request headers -- such as a cookie, or a language, or a user-agent -- you'll need to use the ``Vary`` header to tell caching mechanisms that the page output depends on those things. -To do this in Django, use the convenient ``vary_on_headers`` view decorator, -like so:: +To do this in Django, use the convenient +:func:`django.views.decorators.vary.vary_on_headers` view decorator, like so:: from django.views.decorators.vary import vary_on_headers @@ -1027,8 +1027,9 @@ the user-agent ``Mozilla`` and the cookie value ``foo=bar`` will be considered different from a request with the user-agent ``Mozilla`` and the cookie value ``foo=ham``. -Because varying on cookie is so common, there's a ``vary_on_cookie`` -decorator. These two views are equivalent:: +Because varying on cookie is so common, there's a +:func:`django.views.decorators.vary.vary_on_cookie` decorator. These two views +are equivalent:: @vary_on_cookie def my_view(request): @@ -1041,7 +1042,7 @@ decorator. These two views are equivalent:: The headers you pass to ``vary_on_headers`` are not case sensitive; ``"User-Agent"`` is the same thing as ``"user-agent"``. -You can also use a helper function, ``django.utils.cache.patch_vary_headers``, +You can also use a helper function, :func:`django.utils.cache.patch_vary_headers`, directly. This function sets, or adds to, the ``Vary header``. For example:: from django.utils.cache import patch_vary_headers @@ -1090,8 +1091,9 @@ exclusive. The decorator ensures that the "public" directive is removed if "private" should be set (and vice versa). An example use of the two directives would be a blog site that offers both private and public entries. Public entries may be cached on any shared cache. The following code uses -``patch_cache_control``, the manual way to modify the cache control header -(it is internally called by the ``cache_control`` decorator):: +:func:`django.utils.cache.patch_cache_control`, the manual way to modify the +cache control header (it is internally called by the ``cache_control`` +decorator):: from django.views.decorators.cache import patch_cache_control from django.views.decorators.vary import vary_on_cookie -- cgit v1.3 From 7882c3a67377de7eed7f52a0b91a0a12b2e32655 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 31 May 2013 10:16:06 +0200 Subject: Fixed #20511 -- Corrected link about isolation levels in databases docs Thanks tinodb for the report. --- docs/ref/databases.txt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index b189f4f5f0..4923e4b177 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -141,10 +141,9 @@ Isolation level .. versionadded:: 1.6 Like PostgreSQL itself, Django defaults to the ``READ COMMITTED`` `isolation -level `_. If you need a higher isolation level -such as ``REPEATABLE READ`` or ``SERIALIZABLE``, set it in the -:setting:`OPTIONS` part of your database configuration in -:setting:`DATABASES`:: +level`_. If you need a higher isolation level such as ``REPEATABLE READ`` or +``SERIALIZABLE``, set it in the :setting:`OPTIONS` part of your database +configuration in :setting:`DATABASES`:: import psycopg2.extensions @@ -161,7 +160,7 @@ such as ``REPEATABLE READ`` or ``SERIALIZABLE``, set it in the handle exceptions raised on serialization failures. This option is designed for advanced uses. -.. _postgresql-isolation-levels: http://www.postgresql.org/docs/current/static/transaction-iso.html +.. _isolation level: http://www.postgresql.org/docs/current/static/transaction-iso.html Indexes for ``varchar`` and ``text`` columns -------------------------------------------- -- cgit v1.3 From 646a2216e97a581314c9a2598d481b9e954f2e47 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 31 May 2013 08:07:40 -0400 Subject: Fixed #20326 - Corrected form wizard get_form() example. Thanks tris@ for the report. --- docs/ref/contrib/formtools/form-wizard.txt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index f85ae8356d..7795a32c09 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -420,8 +420,10 @@ Advanced ``WizardView`` methods .. method:: WizardView.get_form(step=None, data=None, files=None) This method constructs the form for a given ``step``. If no ``step`` is - defined, the current step will be determined automatically. - The method gets three arguments: + defined, the current step will be determined automatically. If you override + ``get_form``, however, you will need to set ``step`` yourself using + ``self.steps.current`` as in the example below. The method gets three + arguments: * ``step`` -- The step for which the form instance should be generated. * ``data`` -- Gets passed to the form's data argument @@ -433,6 +435,11 @@ Advanced ``WizardView`` methods def get_form(self, step=None, data=None, files=None): form = super(MyWizard, self).get_form(step, data, files) + + # determine the step if not given + if step is None: + step = self.steps.current + if step == '1': form.user = self.request.user return form -- cgit v1.3 From 61524b09cfa3b51643d0e79cbf0e1e08ede357ae Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 1 Jun 2013 18:16:57 -0400 Subject: Fixed #18388 - Added InlineModelAdmin.get_max_num hook. Thanks d.willy.c.c@ for the suggestion and Melevir and Areski Belaid for work on the patch. --- django/contrib/admin/options.py | 6 +++++- docs/ref/contrib/admin/index.txt | 26 ++++++++++++++++++++++++++ docs/releases/1.6.txt | 5 +++-- tests/admin_inlines/admin.py | 9 +++++++-- tests/admin_inlines/tests.py | 6 ++++++ 5 files changed, 47 insertions(+), 5 deletions(-) (limited to 'docs/ref') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 97f82cbb59..34583ebf74 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1516,6 +1516,10 @@ class InlineModelAdmin(BaseModelAdmin): """Hook for customizing the number of extra inline forms.""" return self.extra + def get_max_num(self, request, obj=None, **kwargs): + """Hook for customizing the max number of extra inline forms.""" + return self.max_num + def get_formset(self, request, obj=None, **kwargs): """Returns a BaseInlineFormSet class for use in admin add/change views.""" if 'fields' in kwargs: @@ -1543,7 +1547,7 @@ class InlineModelAdmin(BaseModelAdmin): "exclude": exclude, "formfield_callback": partial(self.formfield_for_dbfield, request=request), "extra": self.get_extra(request, obj, **kwargs), - "max_num": self.max_num, + "max_num": self.get_max_num(request, obj, **kwargs), "can_delete": can_delete, } diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 73ea74adc0..8f457e77ac 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1728,6 +1728,11 @@ The ``InlineModelAdmin`` class adds: doesn't directly correlate to the number of objects, but can if the value is small enough. See :ref:`model-formsets-max-num` for more information. + .. versionadded:: 1.6 + + :meth:`InlineModelAdmin.get_max_num` also allows you to customize the + maximum number of extra forms. + .. attribute:: InlineModelAdmin.raw_id_fields By default, Django's admin uses a select-box interface (' # The total number of forms will remain the same in either case total_forms_hidden = '' + response = self.client.get('/admin/admin_inlines/binarytree/add/') + self.assertContains(response, max_forms_input % 3) self.assertContains(response, total_forms_hidden) response = self.client.get("/admin/admin_inlines/binarytree/%d/" % bt_head.id) + self.assertContains(response, max_forms_input % 2) self.assertContains(response, total_forms_hidden) -- cgit v1.3 From c36b75c814f068fcb722d46bd5e71cbaddf9bf2d Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 3 Jun 2013 10:06:48 -0400 Subject: Fixed #20545 - Made class-based view MRO lists consistent. Thanks wim@ for the suggestion. --- docs/ref/class-based-views/base.txt | 1 - docs/ref/class-based-views/generic-date-based.txt | 7 ------- docs/ref/class-based-views/generic-display.txt | 1 - docs/ref/class-based-views/generic-editing.txt | 4 ---- 4 files changed, 13 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index 17862978e7..319bd4ebfe 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -114,7 +114,6 @@ TemplateView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.base.TemplateView` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.base.View` diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 4dcb788779..1ebee254b1 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -33,7 +33,6 @@ ArchiveIndexView **Ancestors (MRO)** - * :class:`django.views.generic.dates.ArchiveIndexView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseArchiveIndexView` @@ -100,7 +99,6 @@ YearArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.YearArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseYearArchiveView` @@ -216,7 +214,6 @@ MonthArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.MonthArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseMonthArchiveView` @@ -317,7 +314,6 @@ WeekArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.WeekArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseWeekArchiveView` @@ -422,7 +418,6 @@ DayArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.DayArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseDayArchiveView` @@ -526,7 +521,6 @@ TodayArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.TodayArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseTodayArchiveView` @@ -585,7 +579,6 @@ DateDetailView **Ancestors (MRO)** - * :class:`django.views.generic.dates.DateDetailView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseDateDetailView` diff --git a/docs/ref/class-based-views/generic-display.txt b/docs/ref/class-based-views/generic-display.txt index b827c0005c..c133134d65 100644 --- a/docs/ref/class-based-views/generic-display.txt +++ b/docs/ref/class-based-views/generic-display.txt @@ -77,7 +77,6 @@ ListView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.list.ListView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.list.BaseListView` diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt index 555ba40cfb..c1fb2dcca9 100644 --- a/docs/ref/class-based-views/generic-editing.txt +++ b/docs/ref/class-based-views/generic-editing.txt @@ -36,7 +36,6 @@ FormView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.edit.FormView` * :class:`django.views.generic.base.TemplateResponseMixin` * ``django.views.generic.edit.BaseFormView`` * :class:`django.views.generic.edit.FormMixin` @@ -83,7 +82,6 @@ CreateView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.edit.CreateView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * ``django.views.generic.edit.BaseCreateView`` @@ -126,7 +124,6 @@ UpdateView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.edit.UpdateView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * ``django.views.generic.edit.BaseUpdateView`` @@ -169,7 +166,6 @@ DeleteView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.edit.DeleteView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * ``django.views.generic.edit.BaseDeleteView`` -- cgit v1.3 From 54485557855d58d9a5027511025d5ab22f721c6d Mon Sep 17 00:00:00 2001 From: James Aylett Date: Tue, 4 Jun 2013 12:31:06 +0100 Subject: Fixed #17601 -- expose underlying db exceptions under py2 Use __cause__ to expose the underlying database exceptions even under python 2. --- django/db/utils.py | 3 +-- docs/ref/exceptions.txt | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) (limited to 'docs/ref') diff --git a/django/db/utils.py b/django/db/utils.py index 99804ee8fa..bd7e10d24c 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -91,8 +91,7 @@ class DatabaseErrorWrapper(object): except AttributeError: args = (exc_value,) dj_exc_value = dj_exc_type(*args) - if six.PY3: - dj_exc_value.__cause__ = exc_value + dj_exc_value.__cause__ = exc_value # Only set the 'errors_occurred' flag for errors that may make # the connection unusable. if dj_exc_type not in (DataError, IntegrityError): diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt index 6a5e6f49d5..b15bbea8fa 100644 --- a/docs/ref/exceptions.txt +++ b/docs/ref/exceptions.txt @@ -152,10 +152,16 @@ The Django wrappers for database exceptions behave exactly the same as the underlying database exceptions. See :pep:`249`, the Python Database API Specification v2.0, for further information. +As per :pep:`3134`, a ``__cause__`` attribute is set with the original +(underlying) database exception, allowing access to any additional +information provided. (Note that this attribute is available under +both Python 2 and Python 3, although :pep:`3134` normally only applies +to Python 3.) + .. versionchanged:: 1.6 Previous version of Django only wrapped ``DatabaseError`` and - ``IntegrityError``. + ``IntegrityError``, and did not provide ``__cause__``. .. exception:: models.ProtectedError -- cgit v1.3 From bb863faecd3e1d785933bce7cef7e4b4b28dc3d5 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 5 Jun 2013 12:55:50 -0400 Subject: Proofed the 1.6 release notes --- docs/ref/class-based-views/mixins-editing.txt | 2 +- docs/ref/contrib/admin/index.txt | 5 +- docs/ref/contrib/syndication.txt | 34 +++++----- docs/ref/databases.txt | 2 + docs/ref/forms/models.txt | 2 +- docs/ref/settings.txt | 8 +-- docs/releases/1.6.txt | 93 ++++++++++++++------------- docs/topics/testing/overview.txt | 2 +- 8 files changed, 81 insertions(+), 67 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/class-based-views/mixins-editing.txt b/docs/ref/class-based-views/mixins-editing.txt index 51d8628818..48d363b3b2 100644 --- a/docs/ref/class-based-views/mixins-editing.txt +++ b/docs/ref/class-based-views/mixins-editing.txt @@ -125,7 +125,7 @@ ModelFormMixin This is a required attribute if you are generating the form class automatically (e.g. using ``model``). Omitting this attribute will - result in all fields being used, but this behaviour is deprecated + result in all fields being used, but this behavior is deprecated and will be removed in Django 1.8. .. attribute:: success_url diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 8f457e77ac..11bc2c7268 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1026,8 +1026,9 @@ subclass:: Performs a full-text match. This is like the default search method but uses an index. Currently this is only available for MySQL. - If you need to customize search you can use :meth:`ModelAdmin.get_search_results` to provide additional or alternate - search behaviour. + If you need to customize search you can use + :meth:`ModelAdmin.get_search_results` to provide additional or alternate + search behavior. Custom template options ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 02159c415b..51d038d187 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -137,25 +137,29 @@ into those elements. See `a complex example`_ below that uses a description template. - There is also a way to pass additional information to title and description - templates, if you need to supply more than the two variables mentioned - before. You can provide your implementation of ``get_context_data`` method - in your Feed subclass. For example:: + .. method:: Feed.get_context_data(self, **kwargs) - from mysite.models import Article - from django.contrib.syndication.views import Feed + .. versionadded:: 1.6 - class ArticlesFeed(Feed): - title = "My articles" - description_template = "feeds/articles.html" + There is also a way to pass additional information to title and description + templates, if you need to supply more than the two variables mentioned + before. You can provide your implementation of ``get_context_data`` method + in your ``Feed`` subclass. For example:: - def items(self): - return Article.objects.order_by('-pub_date')[:5] + from mysite.models import Article + from django.contrib.syndication.views import Feed - def get_context_data(self, **kwargs): - context = super(ArticlesFeed, self).get_context_data(**kwargs) - context['foo'] = 'bar' - return context + class ArticlesFeed(Feed): + title = "My articles" + description_template = "feeds/articles.html" + + def items(self): + return Article.objects.order_by('-pub_date')[:5] + + def get_context_data(self, **kwargs): + context = super(ArticlesFeed, self).get_context_data(**kwargs) + context['foo'] = 'bar' + return context And the template: diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 4923e4b177..a648ac1709 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -135,6 +135,8 @@ configuration in :setting:`DATABASES`:: Since Django 1.6, autocommit is turned on by default. This configuration is ignored and can be safely removed. +.. _database-isolation-level: + Isolation level --------------- diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index 54d34160a5..b54056af0c 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -33,7 +33,7 @@ Model Form Functions ``fields`` or ``exclude``, or the corresponding attributes on the form's inner ``Meta`` class. See :ref:`modelforms-selecting-fields` for more information. Omitting any definition of the fields to use will result in all - fields being used, but this behaviour is deprecated. + fields being used, but this behavior is deprecated. The ``localized_fields`` parameter was added. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index b8ebc16bad..ef52d3170c 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -344,9 +344,9 @@ CSRF_COOKIE_HTTPONLY Default: ``False`` -Whether to use HttpOnly flag on the CSRF cookie. If this is set to ``True``, -client-side JavaScript will not to be able to access the CSRF cookie. See -:setting:`SESSION_COOKIE_HTTPONLY` for details on HttpOnly. +Whether to use ``HttpOnly`` flag on the CSRF cookie. If this is set to +``True``, client-side JavaScript will not to be able to access the CSRF cookie. +See :setting:`SESSION_COOKIE_HTTPONLY` for details on ``HttpOnly``. .. setting:: CSRF_COOKIE_NAME @@ -2315,7 +2315,7 @@ SESSION_COOKIE_HTTPONLY Default: ``True`` -Whether to use HTTPOnly flag on the session cookie. If this is set to +Whether to use ``HTTPOnly`` flag on the session cookie. If this is set to ``True``, client-side JavaScript will not to be able to access the session cookie. diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 0970453b18..6736af8c2d 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -79,9 +79,9 @@ location of tests. The previous runner :setting:`INSTALLED_APPS`. The new runner (``django.test.runner.DiscoverRunner``) uses the test discovery -features built into unittest2 (the version of unittest in the Python 2.7+ -standard library, and bundled with Django). With test discovery, tests can be -located in any module whose name matches the pattern ``test*.py``. +features built into ``unittest2`` (the version of ``unittest`` in the +Python 2.7+ standard library, and bundled with Django). With test discovery, +tests can be located in any module whose name matches the pattern ``test*.py``. In addition, the test labels provided to ``./manage.py test`` to nominate specific tests to run must now be full Python dotted paths (or directory @@ -111,7 +111,7 @@ Django 1.6 adds support for savepoints in SQLite, with some :ref:`limitations ``BinaryField`` model field ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A new :class:`django.db.models.BinaryField` model field allows to store raw +A new :class:`django.db.models.BinaryField` model field allows storage of raw binary data in the database. GeoDjango form widgets @@ -127,13 +127,13 @@ Minor features * Authentication backends can raise ``PermissionDenied`` to immediately fail the authentication chain. -* The HttpOnly flag can be set on the CSRF cookie with +* The ``HttpOnly`` flag can be set on the CSRF cookie with :setting:`CSRF_COOKIE_HTTPONLY`. -* The ``assertQuerysetEqual()`` now checks for undefined order and raises - ``ValueError`` if undefined order is spotted. The order is seen as - undefined if the given ``QuerySet`` isn't ordered and there are more than - one ordered values to compare against. +* The :meth:`~django.test.TransactionTestCase.assertQuerysetEqual` now checks + for undefined order and raises :exc:`~exceptions.ValueError` if undefined + order is spotted. The order is seen as undefined if the given ``QuerySet`` + isn't ordered and there are more than one ordered values to compare against. * Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with :meth:`~django.db.models.query.QuerySet.latest`. @@ -146,10 +146,10 @@ Minor features * The default widgets for :class:`~django.forms.EmailField`, :class:`~django.forms.URLField`, :class:`~django.forms.IntegerField`, :class:`~django.forms.FloatField` and :class:`~django.forms.DecimalField` use - the new type attributes available in HTML5 (type='email', type='url', - type='number'). Note that due to erratic support of the ``number`` input type - with localized numbers in current browsers, Django only uses it when numeric - fields are not localized. + the new type attributes available in HTML5 (``type='email'``, ``type='url'``, + ``type='number'``). Note that due to erratic support of the ``number`` + input type with localized numbers in current browsers, Django only uses it + when numeric fields are not localized. * The ``number`` argument for :ref:`lazy plural translations ` can be provided at translation time rather than @@ -185,19 +185,21 @@ Minor features * The jQuery library embedded in the admin has been upgraded to version 1.9.1. * Syndication feeds (:mod:`django.contrib.syndication`) can now pass extra - context through to feed templates using a new `Feed.get_context_data()` - callback. + context through to feed templates using a new + :meth:`Feed.get_context_data() + ` callback. * The admin list columns have a ``column-`` class in the HTML so the columns header can be styled with CSS, e.g. to set a column width. -* The isolation level can be customized under PostgreSQL. +* The :ref:`isolation level` can be customized under + PostgreSQL. * The :ttag:`blocktrans` template tag now respects :setting:`TEMPLATE_STRING_IF_INVALID` for variables not present in the context, just like other template constructs. -* SimpleLazyObjects will now present more helpful representations in shell +* ``SimpleLazyObject``\s will now present more helpful representations in shell debugging situations. * Generic :class:`~django.contrib.gis.db.models.GeometryField` is now editable @@ -210,7 +212,7 @@ Minor features * The documentation contains a :doc:`deployment checklist `. -* The :djadmin:`diffsettings` comand gained a ``--all`` option. +* The :djadmin:`diffsettings` command gained a ``--all`` option. * ``django.forms.fields.Field.__init__`` now calls ``super()``, allowing field mixins to implement ``__init__()`` methods that will reliably be @@ -241,17 +243,19 @@ Minor features * The ``choices`` argument to model fields now accepts an iterable of iterables instead of requiring an iterable of lists or tuples. -* The reason phrase can be customized in HTTP responses. +* The reason phrase can be customized in HTTP responses using + :attr:`~django.http.HttpResponse.reason_phrase`. -* When giving the URL of the next page for :func:`~django.contrib.auth.views.logout`, +* When giving the URL of the next page for + :func:`~django.contrib.auth.views.logout`, :func:`~django.contrib.auth.views.password_reset`, :func:`~django.contrib.auth.views.password_reset_confirm`, and :func:`~django.contrib.auth.views.password_change`, you can now pass URL names and they will be resolved. -* The ``dumpdata`` manage.py command now has a --pks option which will - allow users to specify the primary keys of objects they want to dump. - This option can only be used with one model. +* The :djadmin:`dumpdata` ``manage.py`` command now has a :djadminopt:`--pks` + option which will allow users to specify the primary keys of objects they + want to dump. This option can only be used with one model. * Added ``QuerySet`` methods :meth:`~django.db.models.query.QuerySet.first` and :meth:`~django.db.models.query.QuerySet.last` which are convenience @@ -259,16 +263,18 @@ Minor features ``None`` if there are no objects matching. * :class:`~django.views.generic.base.View` and - :class:`~django.views.generic.base.RedirectView` now support HTTP PATCH method. + :class:`~django.views.generic.base.RedirectView` now support HTTP ``PATCH`` + method. * :class:`GenericForeignKey ` - now takes an optional ``for_concrete_model`` argument, which when set to - ``False`` allows the field to reference proxy models. The default is ``True`` - to retain the old behavior. + now takes an optional + :attr:`~django.contrib.contenttypes.generic.GenericForeignKey.for_concrete_model` + argument, which when set to ``False`` allows the field to reference proxy + models. The default is ``True`` to retain the old behavior. -* The middleware :class:`~django.middleware.locale.LocaleMiddleware` now - stores active language in session if it is not present there. This - prevents loss of language settings after session flush, e.g. logout. +* The :class:`~django.middleware.locale.LocaleMiddleware` now stores the active + language in session if it is not present there. This prevents loss of + language settings after session flush, e.g. logout. * :exc:`~django.core.exceptions.SuspiciousOperation` has been differentiated into a number of subclasses, and each will log to a matching named logger @@ -316,7 +322,7 @@ Behavior changes Database-level autocommit is enabled by default in Django 1.6. While this doesn't change the general spirit of Django's transaction management, there -are a few known backwards-incompatibities, described in the :ref:`transaction +are a few known backwards-incompatibilities, described in the :ref:`transaction management docs `. You should review your code to determine if you're affected. @@ -496,7 +502,7 @@ For Oracle, execute this query: ALTER TABLE DJANGO_COMMENTS MODIFY (ip_address VARCHAR2(39)); -If you do not apply this change, the behaviour is unchanged: on MySQL, IPv6 +If you do not apply this change, the behavior is unchanged: on MySQL, IPv6 addresses are silently truncated; on Oracle, an exception is generated. No database change is needed for SQLite or PostgreSQL databases. @@ -617,11 +623,12 @@ Miscellaneous stored as ``null``. Previously, storing a ``blank`` value in a field which did not allow ``null`` would cause a database exception at runtime. -* If a :class:`~django.core.urlresolvers.NoReverseMatch` exception is risen - from a method when rendering a template it is not silenced. For example - {{ obj.view_href }} will cause template rendering to fail if view_href() - raises NoReverseMatch. There is no change to {% url %} tag, it causes - template rendering to fail like always when NoReverseMatch is risen. +* If a :class:`~django.core.urlresolvers.NoReverseMatch` exception is raised + from a method when rendering a template, it is not silenced. For example, + ``{{ obj.view_href }}`` will cause template rendering to fail if + ``view_href()`` raises ``NoReverseMatch``. There is no change to the + ``{% url %}`` tag, it causes template rendering to fail like always when + ``NoReverseMatch`` is risen. * :meth:`django.test.client.Client.logout` now calls :meth:`django.contrib.auth.logout` which will send the @@ -738,7 +745,7 @@ from your settings. If you defined your own form widgets and defined the ``_has_changed`` method on a widget, you should now define this method on the form field itself. -``module_name`` model meta attribute +``module_name`` model _meta attribute ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``Model._meta.module_name`` was renamed to ``model_name``. Despite being a @@ -780,7 +787,7 @@ particular with boolean fields, it is possible for this problem to be completely invisible. This is a form of `Mass assignment vulnerability `_. -For this reason, this behaviour is deprecated, and using the ``Meta.exclude`` +For this reason, this behavior is deprecated, and using the ``Meta.exclude`` option is strongly discouraged. Instead, all fields that are intended for inclusion in the form should be listed explicitly in the ``fields`` attribute. @@ -799,7 +806,7 @@ is another option. The admin has its own methods for defining fields redundant. Instead, simply omit the ``Meta`` inner class of the ``ModelForm``, or omit the ``Meta.model`` attribute. Since the ``ModelAdmin`` subclass knows which model it is for, it can add the necessary attributes to derive a -functioning ``ModelForm``. This behaviour also works for earlier Django +functioning ``ModelForm``. This behavior also works for earlier Django versions. ``UpdateView`` and ``CreateView`` without explicit fields @@ -821,10 +828,10 @@ deprecated. .. _m2m-help_text-deprecation: -Munging of help text of model form fields for ManyToManyField fields -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Munging of help text of model form fields for ``ManyToManyField`` fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -All special handling of the ``help_text`` attibute of ManyToManyField model +All special handling of the ``help_text`` attribute of ``ManyToManyField`` model fields performed by standard model or model form fields as described in :ref:`m2m-help_text` above is deprecated and will be removed in Django 1.8. diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index d81b171a49..2b1db5e501 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -953,7 +953,7 @@ to test the effects of commit and rollback: key values started at one in :class:`~django.test.TransactionTestCase` tests. - Tests should not depend on this behaviour, but for legacy tests that do, the + Tests should not depend on this behavior, but for legacy tests that do, the :attr:`~TransactionTestCase.reset_sequences` attribute can be used until the test has been properly updated. -- cgit v1.3 From 0fd9f7c95f748764867dc148a2bacef807466d85 Mon Sep 17 00:00:00 2001 From: Tomek Paczkowski Date: Tue, 4 Jun 2013 23:35:11 +0200 Subject: Fixed #19080 -- Fine-grained control over select_related in admin --- django/contrib/admin/validation.py | 10 +++++++-- django/contrib/admin/views/main.py | 44 +++++++++++++++++++++++--------------- docs/ref/contrib/admin/index.txt | 22 ++++++++++++++----- tests/admin_changelist/admin.py | 5 +++++ tests/admin_changelist/tests.py | 31 +++++++++++++++++++++++---- tests/modeladmin/tests.py | 6 ++++-- 6 files changed, 88 insertions(+), 30 deletions(-) (limited to 'docs/ref') diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 59c5ad35ef..222d433e53 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -310,8 +310,14 @@ class ModelAdminValidator(BaseValidator): % (cls.__name__, idx, field)) def validate_list_select_related(self, cls, model): - " Validate that list_select_related is a boolean. " - check_type(cls, 'list_select_related', bool) + " Validate that list_select_related is a boolean, a list or a tuple. " + list_select_related = getattr(cls, 'list_select_related', None) + if list_select_related: + types = (bool, tuple, list) + if not isinstance(list_select_related, types): + raise ImproperlyConfigured("'%s.list_select_related' should be " + "either a bool, a tuple or a list" % + cls.__name__) def validate_list_per_page(self, cls, model): " Validate that list_per_page is an integer. " diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index dbed21265c..8ea7e10fc0 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -356,36 +356,46 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): # ValueError, ValidationError, or ?. raise IncorrectLookupParameters(e) - # Use select_related() if one of the list_display options is a field - # with a relationship and the provided queryset doesn't already have - # select_related defined. if not qs.query.select_related: - if self.list_select_related: - qs = qs.select_related() - else: - for field_name in self.list_display: - try: - field = self.lookup_opts.get_field(field_name) - except models.FieldDoesNotExist: - pass - else: - if isinstance(field.rel, models.ManyToOneRel): - qs = qs.select_related() - break + qs = self.apply_select_related(qs) # Set ordering. ordering = self.get_ordering(request, qs) qs = qs.order_by(*ordering) # Apply search results - qs, search_use_distinct = self.model_admin.get_search_results(request, qs, self.query) + qs, search_use_distinct = self.model_admin.get_search_results( + request, qs, self.query) - # Remove duplicates from results, if neccesary + # Remove duplicates from results, if necessary if filters_use_distinct | search_use_distinct: return qs.distinct() else: return qs + def apply_select_related(self, qs): + if self.list_select_related is True: + return qs.select_related() + + if self.list_select_related is False: + if self.has_related_field_in_list_display(): + return qs.select_related() + + if self.list_select_related: + return qs.select_related(*self.list_select_related) + return qs + + def has_related_field_in_list_display(self): + for field_name in self.list_display: + try: + field = self.lookup_opts.get_field(field_name) + except models.FieldDoesNotExist: + pass + else: + if isinstance(field.rel, models.ManyToOneRel): + return True + return False + def url_for_result(self, result): pk = getattr(result, self.pk_attname) return reverse('admin:%s_%s_change' % (self.opts.app_label, diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 8f457e77ac..1a5e9f2073 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -812,12 +812,24 @@ subclass:: the list of objects on the admin change list page. This can save you a bunch of database queries. - The value should be either ``True`` or ``False``. Default is ``False``. + .. versionchanged:: dev - Note that Django will use - :meth:`~django.db.models.query.QuerySet.select_related`, - regardless of this setting if one of the ``list_display`` fields is a - ``ForeignKey``. + The value should be either a boolean, a list or a tuple. Default is + ``False``. + + When value is ``True``, ``select_related()`` will always be called. When + value is set to ``False``, Django will look at ``list_display`` and call + ``select_related()`` if any ``ForeignKey`` is present. + + If you need more fine-grained control, use a tuple (or list) as value for + ``list_select_related``. Empty tuple will prevent Django from calling + ``select_related`` at all. Any other tuple will be passed directly to + ``select_related`` as parameters. For example:: + + class ArticleAdmin(admin.ModelAdmin): + list_select_related = ('author', 'category') + + will call ``select_related('author', 'category')``. .. attribute:: ModelAdmin.ordering diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index 8387ba77a1..175b1972c9 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -67,6 +67,11 @@ class ChordsBandAdmin(admin.ModelAdmin): list_filter = ['members'] +class InvitationAdmin(admin.ModelAdmin): + list_display = ('band', 'player') + list_select_related = ('player',) + + class DynamicListDisplayChildAdmin(admin.ModelAdmin): list_display = ('parent', 'name', 'age') diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index 38b78faaf4..7f3f0d162e 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -18,7 +18,7 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin, GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin, DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin, FilteredChildAdmin, CustomPaginator, site as custom_site, - SwallowAdmin, DynamicListFilterChildAdmin) + SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin) from .models import (Event, Child, Parent, Genre, Band, Musician, Group, Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow, UnorderedObject, OrderedObject, CustomIdUser) @@ -46,9 +46,32 @@ class ChangeListTests(TestCase): m = ChildAdmin(Child, admin.site) request = self.factory.get('/child/') cl = ChangeList(request, Child, m.list_display, m.list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m) - self.assertEqual(cl.queryset.query.select_related, {'parent': {'name': {}}}) + m.list_filter, m.date_hierarchy, m.search_fields, + m.list_select_related, m.list_per_page, + m.list_max_show_all, m.list_editable, m) + self.assertEqual(cl.queryset.query.select_related, { + 'parent': {'name': {}} + }) + + def test_select_related_as_tuple(self): + ia = InvitationAdmin(Invitation, admin.site) + request = self.factory.get('/invitation/') + cl = ChangeList(request, Child, ia.list_display, ia.list_display_links, + ia.list_filter, ia.date_hierarchy, ia.search_fields, + ia.list_select_related, ia.list_per_page, + ia.list_max_show_all, ia.list_editable, ia) + self.assertEqual(cl.queryset.query.select_related, {'player': {}}) + + def test_select_related_as_empty_tuple(self): + ia = InvitationAdmin(Invitation, admin.site) + ia.list_select_related = () + request = self.factory.get('/invitation/') + cl = ChangeList(request, Child, ia.list_display, ia.list_display_links, + ia.list_filter, ia.date_hierarchy, ia.search_fields, + ia.list_select_related, ia.list_per_page, + ia.list_max_show_all, ia.list_editable, ia) + self.assertEqual(cl.queryset.query.select_related, False) + def test_result_list_empty_changelist_value(self): """ diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 73e88024cd..805b57c070 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -1161,9 +1161,11 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): list_select_related = 1 - six.assertRaisesRegex(self, + six.assertRaisesRegex( + self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_select_related' should be a bool.", + "'ValidationTestModelAdmin.list_select_related' should be either a " + "bool, a tuple or a list", ValidationTestModelAdmin.validate, ValidationTestModel, ) -- cgit v1.3 From 357d62d9f2972bf1bc21e5835c12c849143e06af Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Thu, 6 Jun 2013 11:03:47 -0500 Subject: Explained that timezone.now() always returns times in UTC. The docs were ambiguous about the time zone for now(), leading people to assume that it would be the current time zone rather that UTC. --- docs/ref/utils.txt | 18 +++++++++++++++--- docs/topics/i18n/timezones.txt | 2 ++ 2 files changed, 17 insertions(+), 3 deletions(-) (limited to 'docs/ref') diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index bf14af0855..d2ef945a2e 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -929,9 +929,21 @@ For a complete discussion on the usage of the following see the .. function:: now() - Returns an aware or naive :class:`~datetime.datetime` that represents the - current point in time when :setting:`USE_TZ` is ``True`` or ``False`` - respectively. + Returns a :class:`~datetime.datetime` that represents the + current point in time. Exactly what's returned depends on the value of + :setting:`USE_TZ`: + + * If :setting:`USE_TZ` is ``False``, this will be be a + :ref:`naive ` datetime (i.e. a datetime + without an associated timezone) that represents the current time + in the system's local timezone. + + * If :setting:`USE_TZ` is ``True``, this will be an + :ref:`aware ` datetime representing the + current time in UTC. Note that :func:`now` will always return + times in UTC regardless of the value of :setting:`TIME_ZONE`; + you can use :func:`localtime` to convert to a time in the current + time zone. .. function:: is_aware(value) diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index e4a043b08f..5ed60d0a94 100644 --- a/docs/topics/i18n/timezones.txt +++ b/docs/topics/i18n/timezones.txt @@ -54,6 +54,8 @@ FAQ `. Concepts ======== +.. _naive_vs_aware_datetimes: + Naive and aware datetime objects -------------------------------- -- cgit v1.3