From ee11d325a426480a8549069b2d003bb8eec107f8 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 10:29:01 +0200 Subject: Reorganize committers list chronologically. This completes the removal of the distinction between core devs and specialists. Patch by Simon Meers. --- docs/internals/committers.txt | 187 +++++++++++++++++++++--------------------- 1 file changed, 93 insertions(+), 94 deletions(-) (limited to 'docs') diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 6b9c7df14a..131f9c0f26 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -88,6 +88,23 @@ Malcolm Tredinnick *Malcolm passed away on March 17, 2013.* +`Luke Plant`_ + At University Luke studied physics and Materials Science and also + met `Michael Meeks`_ who introduced him to Linux and Open Source, + re-igniting an interest in programming. Since then he has + contributed to a number of Open Source projects and worked + professionally as a developer. + + Luke has contributed many excellent improvements to Django, + including database-level improvements, the CSRF middleware and + many unit tests. + + Luke currently works for a church in Bradford, UK, and part-time + as a freelance developer. + +.. _luke plant: http://lukeplant.me.uk/ +.. _michael meeks: http://en.wikipedia.org/wiki/Michael_Meeks_(software) + `Russell Keith-Magee`_ Russell studied physics as an undergraduate, and studied neural networks for his PhD. His first job was with a startup in the defense industry developing @@ -102,6 +119,42 @@ Malcolm Tredinnick .. _russell keith-magee: http://cecinestpasun.com/ +`James Bennett`_ + James is Django's release manager, and also contributes to the + documentation and provide the occasional bugfix. + + James came to Web development from philosophy when he discovered + that programmers get to argue just as much while collecting much + better pay. He lives in Lawrence, Kansas and previously worked at + World Online; currently, he's part of the Web development team at + Mozilla. + + He `keeps a blog`_, and enjoys fine port and talking to his car. + +.. _james bennett: http://b-list.org/ +.. _keeps a blog: `james bennett`_ + +`Gary Wilson`_ + Gary starting contributing patches to Django in 2006 while developing Web + applications for `The University of Texas`_ (UT). Since, he has made + contributions to the email and forms systems, as well as many other + improvements and code cleanups throughout the code base. + + Gary is currently a developer and software engineering graduate student at + UT, where his dedication to spreading the ways of Python and Django never + ceases. + + Gary lives in Austin, Texas, USA. + +.. _Gary Wilson: http://thegarywilson.com/ +.. _The University of Texas: http://www.utexas.edu/ + +Matt Boersma + Matt is responsible for Django's Oracle support. + +Ian Kelly + Ian is also responsible for Django's support for Oracle. + Joseph Kocherhans Joseph was the director of lead development at EveryBlock and previously developed at the Lawrence Journal-World. He is treasurer of the `Django @@ -119,23 +172,6 @@ Joseph Kocherhans .. _django software foundation: https://www.djangoproject.com/foundation/ .. _charango: http://en.wikipedia.org/wiki/Charango -`Luke Plant`_ - At University Luke studied physics and Materials Science and also - met `Michael Meeks`_ who introduced him to Linux and Open Source, - re-igniting an interest in programming. Since then he has - contributed to a number of Open Source projects and worked - professionally as a developer. - - Luke has contributed many excellent improvements to Django, - including database-level improvements, the CSRF middleware and - many unit tests. - - Luke currently works for a church in Bradford, UK, and part-time - as a freelance developer. - -.. _luke plant: http://lukeplant.me.uk/ -.. _michael meeks: http://en.wikipedia.org/wiki/Michael_Meeks_(software) - `Brian Rosner`_ Brian is currently the tech lead at Eldarion_ managing and developing Django / Pinax_ based Web sites. He enjoys learning more about programming @@ -153,21 +189,6 @@ Joseph Kocherhans .. _django dose: http://djangodose.com/ .. _pinax: http://pinaxproject.com/ -`Gary Wilson`_ - Gary starting contributing patches to Django in 2006 while developing Web - applications for `The University of Texas`_ (UT). Since, he has made - contributions to the email and forms systems, as well as many other - improvements and code cleanups throughout the code base. - - Gary is currently a developer and software engineering graduate student at - UT, where his dedication to spreading the ways of Python and Django never - ceases. - - Gary lives in Austin, Texas, USA. - -.. _Gary Wilson: http://thegarywilson.com/ -.. _The University of Texas: http://www.utexas.edu/ - Justin Bronn Justin Bronn is a computer scientist and attorney specializing in legal topics related to intellectual property and spatial law. @@ -232,6 +253,15 @@ Karen Tracey .. _Alex Gaynor: http://alexgaynor.net .. _Rdio: http://rdio.com +`Simon Meers`_ + Simon discovered Django 0.96 during his Computer Science PhD research and + has been developing with it full-time ever since. His core code + contributions are mostly in Django's admin application. + + Simon works as a freelance developer based in Wollongong, Australia. + +.. _Simon Meers: http://simonmeers.com/ + `Andrew Godwin`_ Andrew is a freelance Python developer and tinkerer, and has been developing against Django since 2007. He graduated from Oxford University @@ -265,6 +295,18 @@ Ramiro Morales Ramiro lives in Córdoba, Argentina. +`Gabriel Hurley`_ + Gabriel has been working with Django since 2008, shortly after the 1.0 + release. Convinced by his business partner that Python and Django were the + right direction for the company, he couldn't have been more happy with the + decision. His contributions range across many areas in Django, but years of + copy-editing and an eye for detail lead him to be particularly at home + while working on Django's documentation. + + Gabriel works as a web developer in Berkeley, CA, USA. + +.. _gabriel hurley: http://strikeawe.com/ + `Chris Beaven`_ Chris has been submitting patches and suggesting crazy ideas for Django since early 2006. An advocate for community involvement and a long-term @@ -290,6 +332,13 @@ Honza Král .. _Whiskey Media: http://www.whiskeymedia.com/ +Tim Graham + When exploring Web frameworks for an independent study project in the fall + of 2008, Tim discovered Django and was lured to it by the documentation. + He enjoys contributing to the docs because they're awesome. + + Tim works as a software engineer and lives in Philadelphia, PA, USA. + `Idan Gazit`_ As a self-professed design geek, Idan was initially attracted to Django sometime between magic-removal and queryset-refactor. Formally trained @@ -439,6 +488,18 @@ Jeremy Dunck .. _Ultimate Frisbee: http://www.montrealultimate.ca .. _Reptiletech: http://www.reptiletech.com +Donald Stufft + Donald found Python and Django in 2007 while trying to find a language, + and web framework that he really enjoyed using after many years of PHP. He + fell in love with the beauty of Python and the way Django made tasks simple + and easy. His contributions to Django focus primarily on ensuring that it + is and remains a secure web framework. + + Donald currently works at `Nebula Inc`_ as a Software Engineer for their + security team and lives in the Greater Philadelphia Area. + +.. _Nebula Inc: https://www.nebula.com/ + `Daniel Lindsley`_ Pythonista since 2003, Djangonaut since 2006. Daniel started with Django just after the v0.90 release (back when ``Manipulators`` looked good) & fell @@ -453,56 +514,6 @@ Jeremy Dunck .. _`Daniel Lindsley`: http://toastdriven.com/ .. _`Amazon Web Services`: https://aws.amazon.com/ -`James Bennett`_ - James is Django's release manager, and also contributes to the - documentation and provide the occasional bugfix. - - James came to Web development from philosophy when he discovered - that programmers get to argue just as much while collecting much - better pay. He lives in Lawrence, Kansas and previously worked at - World Online; currently, he's part of the Web development team at - Mozilla. - - He `keeps a blog`_, and enjoys fine port and talking to his car. - -.. _james bennett: http://b-list.org/ -.. _keeps a blog: `james bennett`_ - -Ian Kelly - Ian is responsible for Django's support for Oracle. - -Matt Boersma - Matt is also responsible for Django's Oracle support. - -`Simon Meers`_ - Simon discovered Django 0.96 during his Computer Science PhD research and - has been developing with it full-time ever since. His core code - contributions are mostly in Django's admin application. He is also helping - to improve Django's documentation. - - Simon works as a freelance developer based in Wollongong, Australia. - -.. _simon meers: http://simonmeers.com/ - -`Gabriel Hurley`_ - Gabriel has been working with Django since 2008, shortly after the 1.0 - release. Convinced by his business partner that Python and Django were the - right direction for the company, he couldn't have been more happy with the - decision. His contributions range across many areas in Django, but years of - copy-editing and an eye for detail lead him to be particularly at home - while working on Django's documentation. - - Gabriel works as a web developer in Berkeley, CA, USA. - -.. _gabriel hurley: http://strikeawe.com/ - -Tim Graham - When exploring Web frameworks for an independent study project in the fall - of 2008, Tim discovered Django and was lured to it by the documentation. - He enjoys contributing to the docs because they're awesome. - - Tim works as a software engineer and lives in Philadelphia, PA, USA. - Marc Tamlyn Marc started life on the web using Django 1.2 back in 2010, and has never looked back. He was involved with rewriting the class based view @@ -515,18 +526,6 @@ Marc Tamlyn .. _CCBV: http://ccbv.co.uk/ .. _Incuna Ltd: http://incuna.com/ -Donald Stufft - Donald found Python and Django in 2007 while trying to find a language, - and web framework that he really enjoyed using after many years of PHP. He - fell in love with the beauty of Python and the way Django made tasks simple - and easy. His contributions to Django focus primarily on ensuring that it - is and remains a secure web framework. - - Donald currently works at `Nebula Inc`_ as a Software Engineer for their - security team and lives in the Greater Philadelphia Area. - -.. _Nebula Inc: https://www.nebula.com/ - Developers Emeritus =================== -- cgit v1.3 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') 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') 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 be826aafacb9fe266209604116efe128a0b4cdc0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 May 2013 13:51:09 +0200 Subject: Fixed #20402: removed as-limit from uWSGI example. It can fail in hard-to-diagnose ways. --- docs/howto/deployment/wsgi/uwsgi.txt | 1 - 1 file changed, 1 deletion(-) (limited to 'docs') diff --git a/docs/howto/deployment/wsgi/uwsgi.txt b/docs/howto/deployment/wsgi/uwsgi.txt index 5b40d5f2f7..22f39342d6 100644 --- a/docs/howto/deployment/wsgi/uwsgi.txt +++ b/docs/howto/deployment/wsgi/uwsgi.txt @@ -62,7 +62,6 @@ Here's an example command to start a uWSGI server:: --processes=5 \ # number of worker processes --uid=1000 --gid=2000 \ # if root, uwsgi can drop privileges --harakiri=20 \ # respawn processes taking more than 20 seconds - --limit-as=128 \ # limit the project to 128 MB --max-requests=5000 \ # respawn processes after serving 5000 requests --vacuum \ # clear environment on exit --home=/path/to/virtual/env \ # optional path to a virtualenv -- 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') 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') 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') 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 89955cc35f3636684ea6f2a6c9504b35a3050f0f Mon Sep 17 00:00:00 2001 From: Jacob Burch Date: Sat, 18 May 2013 12:54:59 +0200 Subject: Fixed #9595 -- Allow non-expiring cache timeouts. Also, streamline the use of 0 and None between cache backends. --- AUTHORS | 1 + django/core/cache/backends/base.py | 10 +++++++--- django/core/cache/backends/db.py | 15 +++++++++------ django/core/cache/backends/dummy.py | 8 ++++---- django/core/cache/backends/filebased.py | 15 ++++++++------- django/core/cache/backends/locmem.py | 19 +++++++++---------- django/core/cache/backends/memcached.py | 22 ++++++++++++++++------ docs/releases/1.6.txt | 6 ++++++ docs/topics/cache.txt | 13 +++++++++---- tests/cache/tests.py | 28 ++++++++++++++++++++++++++++ 10 files changed, 97 insertions(+), 40 deletions(-) (limited to 'docs') diff --git a/AUTHORS b/AUTHORS index 70f8bb22d9..0ad6522725 100644 --- a/AUTHORS +++ b/AUTHORS @@ -123,6 +123,7 @@ answer newbie questions, and generally made Django that much better: bthomas btoll@bestweb.net Jonathan Buchanan + Jacob Burch Keith Bussell C8E Chris Cahoon diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index 53b0270a57..deb98e7714 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -15,6 +15,10 @@ class CacheKeyWarning(DjangoRuntimeWarning): pass +# Stub class to ensure not passing in a `timeout` argument results in +# the default timeout +DEFAULT_TIMEOUT = object() + # Memcached does not accept keys longer than this. MEMCACHE_MAX_KEY_LENGTH = 250 @@ -84,7 +88,7 @@ class BaseCache(object): new_key = self.key_func(key, self.key_prefix, version) return new_key - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): """ Set a value in the cache if the key does not already exist. If timeout is given, that timeout will be used for the key; otherwise @@ -101,7 +105,7 @@ class BaseCache(object): """ raise NotImplementedError - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): """ Set a value in the cache. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. @@ -163,7 +167,7 @@ class BaseCache(object): # if a subclass overrides it. return self.has_key(key) - def set_many(self, data, timeout=None, version=None): + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): """ Set a bunch of values in the cache at once from a dict of key/value pairs. For certain backends (memcached), this is much more efficient diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index 7749284122..0e59c6d65e 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -9,7 +9,7 @@ except ImportError: import pickle from django.conf import settings -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.db import connections, transaction, router, DatabaseError from django.utils import timezone, six from django.utils.encoding import force_bytes @@ -65,6 +65,7 @@ class DatabaseCache(BaseDatabaseCache): if row is None: return default now = timezone.now() + if row[2] < now: db = router.db_for_write(self.cache_model_class) cursor = connections[db].cursor() @@ -74,18 +75,18 @@ class DatabaseCache(BaseDatabaseCache): value = connections[db].ops.process_clob(row[1]) return pickle.loads(base64.b64decode(force_bytes(value))) - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) self._base_set('set', key, value, timeout) - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) return self._base_set('add', key, value, timeout) - def _base_set(self, mode, key, value, timeout=None): - if timeout is None: + def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT): + if timeout == DEFAULT_TIMEOUT: timeout = self.default_timeout db = router.db_for_write(self.cache_model_class) table = connections[db].ops.quote_name(self._table) @@ -95,7 +96,9 @@ class DatabaseCache(BaseDatabaseCache): num = cursor.fetchone()[0] now = timezone.now() now = now.replace(microsecond=0) - if settings.USE_TZ: + if timeout is None: + exp = datetime.max + elif settings.USE_TZ: exp = datetime.utcfromtimestamp(time.time() + timeout) else: exp = datetime.fromtimestamp(time.time() + timeout) diff --git a/django/core/cache/backends/dummy.py b/django/core/cache/backends/dummy.py index 9fe9b3b5f5..7ca6114ee4 100644 --- a/django/core/cache/backends/dummy.py +++ b/django/core/cache/backends/dummy.py @@ -1,12 +1,12 @@ "Dummy cache backend" -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT class DummyCache(BaseCache): def __init__(self, host, *args, **kwargs): BaseCache.__init__(self, *args, **kwargs) - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) return True @@ -16,7 +16,7 @@ class DummyCache(BaseCache): self.validate_key(key) return default - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) @@ -32,7 +32,7 @@ class DummyCache(BaseCache): self.validate_key(key) return False - def set_many(self, data, timeout=0, version=None): + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): pass def delete_many(self, keys, version=None): diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py index 96194d458f..d19eed4a95 100644 --- a/django/core/cache/backends/filebased.py +++ b/django/core/cache/backends/filebased.py @@ -9,9 +9,10 @@ try: except ImportError: import pickle -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils.encoding import force_bytes + class FileBasedCache(BaseCache): def __init__(self, dir, params): BaseCache.__init__(self, params) @@ -19,7 +20,7 @@ class FileBasedCache(BaseCache): if not os.path.exists(self._dir): self._createdir() - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): if self.has_key(key, version=version): return False @@ -35,7 +36,7 @@ class FileBasedCache(BaseCache): with open(fname, 'rb') as f: exp = pickle.load(f) now = time.time() - if exp < now: + if exp is not None and exp < now: self._delete(fname) else: return pickle.load(f) @@ -43,14 +44,14 @@ class FileBasedCache(BaseCache): pass return default - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) fname = self._key_to_file(key) dirname = os.path.dirname(fname) - if timeout is None: + if timeout == DEFAULT_TIMEOUT: timeout = self.default_timeout self._cull() @@ -60,8 +61,8 @@ class FileBasedCache(BaseCache): os.makedirs(dirname) with open(fname, 'wb') as f: - now = time.time() - pickle.dump(now + timeout, f, pickle.HIGHEST_PROTOCOL) + expiry = None if timeout is None else time.time() + timeout + pickle.dump(expiry, f, pickle.HIGHEST_PROTOCOL) pickle.dump(value, f, pickle.HIGHEST_PROTOCOL) except (IOError, OSError): pass diff --git a/django/core/cache/backends/locmem.py b/django/core/cache/backends/locmem.py index 76667e9609..1fa17052fd 100644 --- a/django/core/cache/backends/locmem.py +++ b/django/core/cache/backends/locmem.py @@ -6,7 +6,7 @@ try: except ImportError: import pickle -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils.synch import RWLock # Global in-memory store of cache data. Keyed by name, to provide @@ -23,7 +23,7 @@ class LocMemCache(BaseCache): self._expire_info = _expire_info.setdefault(name, {}) self._lock = _locks.setdefault(name, RWLock()) - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) with self._lock.writer(): @@ -41,10 +41,8 @@ class LocMemCache(BaseCache): key = self.make_key(key, version=version) self.validate_key(key) with self._lock.reader(): - exp = self._expire_info.get(key) - if exp is None: - return default - elif exp > time.time(): + exp = self._expire_info.get(key, 0) + if exp is None or exp > time.time(): try: pickled = self._cache[key] return pickle.loads(pickled) @@ -58,15 +56,16 @@ class LocMemCache(BaseCache): pass return default - def _set(self, key, value, timeout=None): + def _set(self, key, value, timeout=DEFAULT_TIMEOUT): if len(self._cache) >= self._max_entries: self._cull() - if timeout is None: + if timeout == DEFAULT_TIMEOUT: timeout = self.default_timeout + expiry = None if timeout is None else time.time() + timeout self._cache[key] = value - self._expire_info[key] = time.time() + timeout + self._expire_info[key] = expiry - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) with self._lock.writer(): diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index c942acd52f..64d1c41dc5 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -4,7 +4,7 @@ import time import pickle from threading import local -from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils import six from django.utils.encoding import force_str @@ -36,12 +36,22 @@ class BaseMemcachedCache(BaseCache): return self._client - def _get_memcache_timeout(self, timeout): + def _get_memcache_timeout(self, timeout=DEFAULT_TIMEOUT): """ Memcached deals with long (> 30 days) timeouts in a special way. Call this function to obtain a safe value for your timeout. """ - timeout = timeout or self.default_timeout + if timeout == DEFAULT_TIMEOUT: + return self.default_timeout + + if timeout is None: + # Using 0 in memcache sets a non-expiring timeout. + return 0 + elif int(timeout) == 0: + # Other cache backends treat 0 as set-and-expire. To achieve this + # in memcache backends, a negative timeout must be passed. + timeout = -1 + if timeout > 2592000: # 60*60*24*30, 30 days # See http://code.google.com/p/memcached/wiki/FAQ # "You can set expire times up to 30 days in the future. After that @@ -56,7 +66,7 @@ class BaseMemcachedCache(BaseCache): # Python 2 memcache requires the key to be a byte string. return force_str(super(BaseMemcachedCache, self).make_key(key, version)) - def add(self, key, value, timeout=0, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) return self._cache.add(key, value, self._get_memcache_timeout(timeout)) @@ -67,7 +77,7 @@ class BaseMemcachedCache(BaseCache): return default return val - def set(self, key, value, timeout=0, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self._cache.set(key, value, self._get_memcache_timeout(timeout)) @@ -125,7 +135,7 @@ class BaseMemcachedCache(BaseCache): raise ValueError("Key '%s' not found" % key) return val - def set_many(self, data, timeout=0, version=None): + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): safe_data = {} for key, value in data.items(): key = self.make_key(key, version=version) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index c9b3715c45..60b3381dd6 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -485,6 +485,12 @@ Miscellaneous changes in 1.6 particularly affect :class:`~django.forms.DecimalField` and :class:`~django.forms.ModelMultipleChoiceField`. +* There have been changes in the way timeouts are handled in cache backends. + Explicitly passing in ``timeout=None`` no longer results in using the + default timeout. It will now set a non-expiring timeout. Passing 0 into the + memcache backend no longer uses the default timeout, and now will + set-and-expire-immediately the value. + Features deprecated in 1.6 ========================== diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 6b6d57511a..a7d54fbeb0 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -707,10 +707,15 @@ The basic interface is ``set(key, value, timeout)`` and ``get(key)``:: >>> cache.get('my_key') 'hello, world!' -The ``timeout`` argument is optional and defaults to the ``timeout`` -argument of the appropriate backend in the :setting:`CACHES` setting -(explained above). It's the number of seconds the value should be stored -in the cache. +The ``timeout`` argument is optional and defaults to the ``timeout`` argument +of the appropriate backend in the :setting:`CACHES` setting (explained above). +It's the number of seconds the value should be stored in the cache. Passing in +``None`` for ``timeout`` will cache the value forever. + +.. versionchanged:: 1.6 + + Previously, passing ``None`` explicitly would use the default timeout + value. If the object doesn't exist in the cache, ``cache.get()`` returns ``None``:: diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 00c51638b7..4f7ee8b525 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -441,6 +441,34 @@ class BaseCacheTests(object): self.assertEqual(self.cache.get('key3'), 'sausage') self.assertEqual(self.cache.get('key4'), 'lobster bisque') + def test_forever_timeout(self): + ''' + Passing in None into timeout results in a value that is cached forever + ''' + self.cache.set('key1', 'eggs', None) + self.assertEqual(self.cache.get('key1'), 'eggs') + + self.cache.add('key2', 'ham', None) + self.assertEqual(self.cache.get('key2'), 'ham') + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, None) + self.assertEqual(self.cache.get('key3'), 'sausage') + self.assertEqual(self.cache.get('key4'), 'lobster bisque') + + def test_zero_timeout(self): + ''' + Passing in None into timeout results in a value that is cached forever + ''' + self.cache.set('key1', 'eggs', 0) + self.assertEqual(self.cache.get('key1'), None) + + self.cache.add('key2', 'ham', 0) + self.assertEqual(self.cache.get('key2'), None) + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, 0) + self.assertEqual(self.cache.get('key3'), None) + self.assertEqual(self.cache.get('key4'), None) + def test_float_timeout(self): # Make sure a timeout given as a float doesn't crash anything. self.cache.set("key1", "spam", 100.2) -- cgit v1.3 From d5ce2ff5e485bf94fcade340bc803ba4671bd95a Mon Sep 17 00:00:00 2001 From: Erik Romijn Date: Sat, 18 May 2013 16:35:39 +0200 Subject: Fixed #20444 -- Cookie-based sessions does not include a remote code execution-warning --- docs/topics/http/sessions.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'docs') diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index acad61eb2a..0f2955fadd 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -125,6 +125,17 @@ and the :setting:`SECRET_KEY` setting. .. warning:: + **If the :setting:`SECRET_KEY` is not kept secret, this can lead to + arbitrary remote code execution.** + + An attacker in possession of the :setting:`SECRET_KEY` can not only + generate falsified session data, which your site will trust, but also + remotely execute arbitrary code, as the data is serialized using pickle. + + If you use cookie-based sessions, pay extra care that your secret key is + always kept completely secret, for any system which might be remotely + accessible. + **The session data is signed but not encrypted** When using the cookies backend the session data can be read by the client. -- cgit v1.3 From 64e11a68f19793d11915e83574b1bb693da3980e Mon Sep 17 00:00:00 2001 From: Łukasz Langa Date: Sat, 18 May 2013 16:38:11 +0200 Subject: Fixed #13285: populate_xheaders breaks caching --- django/contrib/flatpages/views.py | 2 - django/core/xheaders.py | 24 --------- docs/releases/1.6.txt | 5 ++ tests/special_headers/__init__.py | 0 tests/special_headers/fixtures/data.xml | 20 ------- tests/special_headers/models.py | 5 -- .../templates/special_headers/article_detail.html | 1 - tests/special_headers/tests.py | 62 ---------------------- tests/special_headers/urls.py | 13 ----- tests/special_headers/views.py | 21 -------- 10 files changed, 5 insertions(+), 148 deletions(-) delete mode 100644 django/core/xheaders.py delete mode 100644 tests/special_headers/__init__.py delete mode 100644 tests/special_headers/fixtures/data.xml delete mode 100644 tests/special_headers/models.py delete mode 100644 tests/special_headers/templates/special_headers/article_detail.html delete mode 100644 tests/special_headers/tests.py delete mode 100644 tests/special_headers/urls.py delete mode 100644 tests/special_headers/views.py (limited to 'docs') diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index 497979e497..20e930f343 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -1,7 +1,6 @@ from django.conf import settings from django.contrib.flatpages.models import FlatPage from django.contrib.sites.models import get_current_site -from django.core.xheaders import populate_xheaders from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect from django.shortcuts import get_object_or_404 from django.template import loader, RequestContext @@ -70,5 +69,4 @@ def render_flatpage(request, f): 'flatpage': f, }) response = HttpResponse(t.render(c)) - populate_xheaders(request, response, FlatPage, f.id) return response diff --git a/django/core/xheaders.py b/django/core/xheaders.py deleted file mode 100644 index 3766628c98..0000000000 --- a/django/core/xheaders.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Pages in Django can are served up with custom HTTP headers containing useful -information about those pages -- namely, the content type and object ID. - -This module contains utility functions for retrieving and doing interesting -things with these special "X-Headers" (so called because the HTTP spec demands -that custom headers are prefixed with "X-"). - -Next time you're at slashdot.org, watch out for X-Fry and X-Bender. :) -""" - -def populate_xheaders(request, response, model, object_id): - """ - Adds the "X-Object-Type" and "X-Object-Id" headers to the given - HttpResponse according to the given model and object_id -- but only if the - given HttpRequest object has an IP address within the INTERNAL_IPS setting - or if the request is from a logged in staff member. - """ - from django.conf import settings - if (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS - or (hasattr(request, 'user') and request.user.is_active - and request.user.is_staff)): - response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.model_name) - response['X-Object-Id'] = str(object_id) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 60b3381dd6..452baf7f76 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -491,6 +491,11 @@ Miscellaneous memcache backend no longer uses the default timeout, and now will set-and-expire-immediately the value. +* The ``django.contrib.flatpages`` app used to set custom HTTP headers for + debugging purposes. This functionality was not documented and made caching + ineffective so it has been removed, along with its generic implementation, + previously available in ``django.core.xheaders``. + Features deprecated in 1.6 ========================== diff --git a/tests/special_headers/__init__.py b/tests/special_headers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/special_headers/fixtures/data.xml b/tests/special_headers/fixtures/data.xml deleted file mode 100644 index 7e60d45199..0000000000 --- a/tests/special_headers/fixtures/data.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - super - Super - User - super@example.com - sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158 - True - True - True - 2007-05-30 13:20:10 - 2007-05-30 13:20:10 - - - - - text - - diff --git a/tests/special_headers/models.py b/tests/special_headers/models.py deleted file mode 100644 index e05c5a6920..0000000000 --- a/tests/special_headers/models.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.db import models - - -class Article(models.Model): - text = models.TextField() diff --git a/tests/special_headers/templates/special_headers/article_detail.html b/tests/special_headers/templates/special_headers/article_detail.html deleted file mode 100644 index 3cbd38cb7e..0000000000 --- a/tests/special_headers/templates/special_headers/article_detail.html +++ /dev/null @@ -1 +0,0 @@ -{{ object }} diff --git a/tests/special_headers/tests.py b/tests/special_headers/tests.py deleted file mode 100644 index b4b704ae21..0000000000 --- a/tests/special_headers/tests.py +++ /dev/null @@ -1,62 +0,0 @@ -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 SpecialHeadersTest(TestCase): - fixtures = ['data.xml'] - urls = 'special_headers.urls' - - def test_xheaders(self): - user = User.objects.get(username='super') - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - self.client.login(username='super', password='secret') - response = self.client.get('/special_headers/article/1/') - self.assertTrue('X-Object-Type' in response) - user.is_staff = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - - def test_xview_func(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/func/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.xview') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - - def test_xview_class(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/class/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.XViewClass') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) diff --git a/tests/special_headers/urls.py b/tests/special_headers/urls.py deleted file mode 100644 index f7ba141207..0000000000 --- a/tests/special_headers/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -# coding: utf-8 -from __future__ import absolute_import - -from django.conf.urls import patterns - -from . import views -from .models import Article - -urlpatterns = patterns('', - (r'^special_headers/article/(?P\d+)/$', views.xview_xheaders), - (r'^special_headers/xview/func/$', views.xview_dec(views.xview)), - (r'^special_headers/xview/class/$', views.xview_dec(views.XViewClass.as_view())), -) diff --git a/tests/special_headers/views.py b/tests/special_headers/views.py deleted file mode 100644 index a8bbd6542e..0000000000 --- a/tests/special_headers/views.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.core.xheaders import populate_xheaders -from django.http import HttpResponse -from django.utils.decorators import decorator_from_middleware -from django.views.generic import View -from django.middleware.doc import XViewMiddleware - -from .models import Article - -xview_dec = decorator_from_middleware(XViewMiddleware) - -def xview(request): - return HttpResponse() - -def xview_xheaders(request, object_id): - response = HttpResponse() - populate_xheaders(request, response, Article, 1) - return response - -class XViewClass(View): - def get(self, request): - return HttpResponse() -- 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') 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') 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 96cabba80805ddda9c48307cb3d75177dc94ba29 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 18 May 2013 11:20:02 -0400 Subject: Fixed #20335 - Documented the {% language %} template tag. Thanks bmispelon for the suggestion and djangsters for the patch. --- docs/topics/i18n/translation.txt | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 5b4ffea528..72e000a86f 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -722,6 +722,31 @@ or with the ``{#`` ... ``#}`` :ref:`one-line comment constructs +

{% trans "Welcome to our page" %}

+ + {% language 'en' %} + {% get_current_language as LANGUAGE_CODE %} + +

{% trans "Welcome to our page" %}

+ {% endlanguage %} + +While the first occurrence of "Welcome to our page" uses the current language, +the second will always be in English. + .. _template-translation-vars: Other tags @@ -1126,13 +1151,11 @@ active language. Example:: .. _reversing_in_templates: -.. templatetag:: language - Reversing in templates ---------------------- If localized URLs get reversed in templates they always use the current -language. To link to a URL in another language use the ``language`` +language. To link to a URL in another language use the :ttag:`language` template tag. It enables the given language in the enclosed template section: .. code-block:: html+django -- 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') 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 2c84f4434cd0f6e74471d5737b049c648cff538f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 18 May 2013 12:05:35 -0400 Subject: Fixed #20284 - Added a note about DEBUG in static file docs. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks wim@ for the suggestion and Nicolas Noé for the patch. --- docs/howto/static-files/index.txt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/howto/static-files/index.txt b/docs/howto/static-files/index.txt index 1fdad94143..3668c5dc41 100644 --- a/docs/howto/static-files/index.txt +++ b/docs/howto/static-files/index.txt @@ -35,8 +35,20 @@ Configuring static files 4. Store your static files in a folder called ``static`` in your app. For example ``my_app/static/my_app/myimage.jpg``. -Now, if you use ``./manage.py runserver``, all static files should be served -automatically at the :setting:`STATIC_URL` and be shown correctly. +.. admonition:: Serving the files + + In addition to these configuration steps, you'll also need to actually + serve the static files. + + During development, this will be done automatically if you use + :djadmin:`runserver` and :setting:`DEBUG` is set to ``True`` (see + :func:`django.contrib.staticfiles.views.serve`). + + This method is **grossly inefficient** and probably **insecure**, + so it is **unsuitable for production**. + + See :doc:`/howto/static-files/deployment` for proper strategies to serve + static files in production environments. Your project will probably also have static assets that aren't tied to a particular app. In addition to using a ``static/`` directory inside your apps, -- 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') 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') 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 ddd9ee16fa0c7cc19fd472fd7c1f6d28693a89be Mon Sep 17 00:00:00 2001 From: leandrafinger Date: Sat, 18 May 2013 13:34:29 +0200 Subject: Add missing imports to the examples in the 'First Steps' --- docs/intro/overview.txt | 4 ++++ docs/intro/tutorial01.txt | 2 ++ docs/intro/tutorial02.txt | 16 ++++++++++++++++ docs/intro/tutorial03.txt | 5 +++++ docs/intro/tutorial04.txt | 2 ++ 5 files changed, 29 insertions(+) (limited to 'docs') diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index 8753817256..1a6c8fa19a 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -24,6 +24,8 @@ representing your models -- so far, it's been solving two years' worth of database-schema problems. Here's a quick example, which might be saved in the file ``mysite/news/models.py``:: + from django.db import models + class Reporter(models.Model): full_name = models.CharField(max_length=70) @@ -214,6 +216,8 @@ Generally, a view retrieves data according to the parameters, loads a template and renders the template with the retrieved data. Here's an example view for ``year_archive`` from above:: + from django.shortcuts import render_to_response + def year_archive(request, year): a_list = Article.objects.filter(pub_date__year=year) return render_to_response('news/year_archive.html', {'year': year, 'article_list': a_list}) diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index d623bd8451..6e5988b15a 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -582,6 +582,8 @@ of this object. Let's fix that by editing the polls model (in the ``Choice``. On Python 3, simply replace ``__unicode__`` by ``__str__`` in the following example:: + from django.db import models + class Poll(models.Model): # ... def __unicode__(self): # Python 3: def __str__(self): diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index 1987c51a67..dd3e86d8ae 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -158,6 +158,9 @@ you want when you register the object. Let's see how this works by re-ordering the fields on the edit form. Replace the ``admin.site.register(Poll)`` line with:: + from django.contrib import admin + from polls.models import Poll + class PollAdmin(admin.ModelAdmin): fields = ['pub_date', 'question'] @@ -179,6 +182,9 @@ of fields, choosing an intuitive order is an important usability detail. And speaking of forms with dozens of fields, you might want to split the form up into fieldsets:: + from django.contrib import admin + from polls.models import Poll + class PollAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question']}), @@ -198,6 +204,9 @@ You can assign arbitrary HTML classes to each fieldset. Django provides a This is useful when you have a long form that contains a number of fields that aren't commonly used:: + from django.contrib import admin + from polls.models import Poll + class PollAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question']}), @@ -218,6 +227,7 @@ Yet. There are two ways to solve this problem. The first is to register ``Choice`` with the admin just as we did with ``Poll``. That's easy:: + from django.contrib import admin from polls.models import Choice admin.site.register(Choice) @@ -342,6 +352,12 @@ representation of the output. You can improve that by giving that method (in :file:`polls/models.py`) a few attributes, as follows:: + import datetime + from django.utils import timezone + from django.db import models + + from polls.models import Poll + class Poll(models.Model): # ... def was_published_recently(self): diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 86cc5f97e6..0bbfcdd02f 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -393,6 +393,9 @@ Now, let's tackle the poll detail view -- the page that displays the question for a given poll. Here's the view:: from django.http import Http404 + from django.shortcuts import render + + from polls.models import Poll # ... def detail(request, poll_id): try: @@ -420,6 +423,8 @@ and raise :exc:`~django.http.Http404` if the object doesn't exist. Django provides a shortcut. Here's the ``detail()`` view, rewritten:: from django.shortcuts import render, get_object_or_404 + + from polls.models import Poll # ... def detail(request, poll_id): poll = get_object_or_404(Poll, pk=poll_id) diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 9f54243a3e..f81a7d6758 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -136,6 +136,8 @@ object. For more on :class:`~django.http.HttpRequest` objects, see the After somebody votes in a poll, the ``vote()`` view redirects to the results page for the poll. Let's write that view:: + from django.shortcuts import get_object_or_404, render + def results(request, poll_id): poll = get_object_or_404(Poll, pk=poll_id) return render(request, 'polls/results.html', {'poll': poll}) -- 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') 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 0b3a6ead88c84aa30f11ad51ad3d89a016f9dd67 Mon Sep 17 00:00:00 2001 From: postrational Date: Sun, 19 May 2013 10:48:30 +0200 Subject: refs #20233 - Full custom user model example isn't really full Addition and fix for custom user model example documentation. https://code.djangoproject.com/ticket/20233 --- docs/topics/auth/customizing.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 56f3e60350..4b6721ba21 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -1075,7 +1075,6 @@ code would be required in the app's ``admin.py`` file:: (None, {'fields': ('email', 'password')}), ('Personal info', {'fields': ('date_of_birth',)}), ('Permissions', {'fields': ('is_admin',)}), - ('Important dates', {'fields': ('last_login',)}), ) add_fieldsets = ( (None, { @@ -1092,3 +1091,8 @@ code would be required in the app's ``admin.py`` file:: # ... and, since we're not using Django's builtin permissions, # unregister the Group model from admin. admin.site.unregister(Group) + +Finally specify the custom model as the default user model for your project using the :setting:`AUTH_USER_MODEL` setting in your ``settings.py``:: + + AUTH_USER_MODEL = 'customauth.MyUser' + -- cgit v1.3 From d9c01da1f8ba3d4e0947fd1d619c8f4a9b013360 Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Sat, 18 May 2013 18:37:39 +0200 Subject: Improve cookie based session backend docs. Note the don't require sessions to be in installed apps. --- docs/topics/http/sessions.txt | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'docs') diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 0f2955fadd..268055b6fc 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -118,6 +118,13 @@ To use cookies-based sessions, set the :setting:`SESSION_ENGINE` setting to stored using Django's tools for :doc:`cryptographic signing ` and the :setting:`SECRET_KEY` setting. +.. note:: + + When using cookies-based sessions :mod:`django.contrib.sessions` can be + removed from :setting:`INSTALLED_APPS` setting because data is loaded + from the key itself and not from the database, so there is no need for the + creation and usage of ``django.contrib.sessions.models.Session`` table. + .. note:: It's recommended to leave the :setting:`SESSION_COOKIE_HTTPONLY` setting -- 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') 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 753edfa4b5cf46be5fc1f04d80c25f30b18af798 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 19 May 2013 12:14:19 +0200 Subject: Fixed a rest mistake I introduced in d5ce2ff. --- docs/topics/http/sessions.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index 268055b6fc..772ee122d5 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -132,8 +132,8 @@ and the :setting:`SECRET_KEY` setting. .. warning:: - **If the :setting:`SECRET_KEY` is not kept secret, this can lead to - arbitrary remote code execution.** + **If the SECRET_KEY is not kept secret, this can lead to arbitrary remote + code execution.** An attacker in possession of the :setting:`SECRET_KEY` can not only generate falsified session data, which your site will trust, but also -- cgit v1.3 From cc62cbed76daeaea28c1e4892244bcf1e148f373 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 19 May 2013 06:51:55 -0400 Subject: Fixed some line wrapping, refs #20233 --- docs/topics/auth/customizing.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 4b6721ba21..9a8bab947c 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -1092,7 +1092,7 @@ code would be required in the app's ``admin.py`` file:: # unregister the Group model from admin. admin.site.unregister(Group) -Finally specify the custom model as the default user model for your project using the :setting:`AUTH_USER_MODEL` setting in your ``settings.py``:: +Finally, specify the custom model as the default user model for your project +using the :setting:`AUTH_USER_MODEL` setting in your ``settings.py``:: AUTH_USER_MODEL = 'customauth.MyUser' - -- 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') 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') 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') 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') 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 1d543949d7acc93a172e8a2c9272d8b983a421ef Mon Sep 17 00:00:00 2001 From: Silvan Spross Date: Sun, 19 May 2013 12:42:44 +0200 Subject: Add missing imports and models to the examples in internationalization and localization documentation --- docs/topics/i18n/translation.txt | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 72e000a86f..433d40b0bb 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -80,6 +80,7 @@ In this example, the text ``"Welcome to my site."`` is marked as a translation string:: from django.utils.translation import ugettext as _ + from django.http import HttpResponse def my_view(request): output = _("Welcome to my site.") @@ -89,6 +90,7 @@ Obviously, you could code this without using the alias. This example is identical to the previous one:: from django.utils.translation import ugettext + from django.http import HttpResponse def my_view(request): output = ugettext("Welcome to my site.") @@ -192,6 +194,7 @@ of its value.) For example:: from django.utils.translation import ungettext + from django.http import HttpResponse def hello_world(request, count): page = ungettext( @@ -208,6 +211,7 @@ languages as the ``count`` variable. Lets see a slightly more complex usage example:: from django.utils.translation import ungettext + from myapp.models import Report count = Report.objects.count() if count == 1: @@ -283,6 +287,7 @@ For example:: or:: + from django.db import models from django.utils.translation import pgettext_lazy class MyThing(models.Model): @@ -328,6 +333,7 @@ Model fields and relationships ``verbose_name`` and ``help_text`` option values For example, to translate the help text of the *name* field in the following model, do the following:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -336,8 +342,6 @@ model, do the following:: You can mark names of ``ForeignKey``, ``ManyTomanyField`` or ``OneToOneField`` relationship as translatable by using their ``verbose_name`` options:: - from django.utils.translation import ugettext_lazy as _ - class MyThing(models.Model): kind = models.ForeignKey(ThingKind, related_name='kinds', verbose_name=_('kind')) @@ -355,6 +359,7 @@ It is recommended to always provide explicit relying on the fallback English-centric and somewhat naïve determination of verbose names Django performs by looking at the model's class name:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -370,6 +375,7 @@ Model methods ``short_description`` attribute values For model methods, you can provide translations to Django and the admin site with the ``short_description`` attribute:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -404,6 +410,7 @@ If you ever see output that looks like ``"hello If you don't like the long ``ugettext_lazy`` name, you can just alias it as ``_`` (underscore), like so:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -429,6 +436,9 @@ definition. Therefore, you are authorized to pass a key name instead of an integer as the ``number`` argument. Then ``number`` will be looked up in the dictionary under that key during string interpolation. Here's example:: + from django import forms + from django.utils.translation import ugettext_lazy + class MyForm(forms.Form): error_message = ungettext_lazy("You only provided %(num)d argument", "You only provided %(num)d arguments", 'num') @@ -461,6 +471,7 @@ that concatenates its contents *and* converts them to strings only when the result is included in a string. For example:: from django.utils.translation import string_concat + from django.utils.translation import ugettext_lazy ... name = ugettext_lazy('John Lennon') instrument = ugettext_lazy('guitar') @@ -1663,6 +1674,8 @@ preference available as ``request.LANGUAGE_CODE`` for each :class:`~django.http.HttpRequest`. Feel free to read this value in your view code. Here's a simple example:: + from django.http import HttpResponse + def hello_world(request, count): if request.LANGUAGE_CODE == 'de-at': return HttpResponse("You prefer to read Austrian German.") -- 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') 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') 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') 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 980ae2ab29cb25182404eb1ccc919573edcc44f5 Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sun, 19 May 2013 12:57:06 +0200 Subject: Fix #20447: URL names given to contrib.auth.views are now resolved. This commit also adds tests for the redirect feature of most auth views. It also cleans up the tests, most notably using @override_settings instead of ad-hoc setUp/tearDown methods. Thanks to caumons for the report. Conflicts: docs/releases/1.6.txt --- django/contrib/auth/tests/test_views.py | 214 +++++++++++++++++++++++--------- django/contrib/auth/tests/urls.py | 11 ++ django/contrib/auth/views.py | 9 ++ docs/releases/1.6.txt | 6 + 4 files changed, 180 insertions(+), 60 deletions(-) (limited to 'docs') diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py index 7cbf72327e..cd045fb793 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -1,6 +1,10 @@ import itertools import os import re +try: + from urllib.parse import urlparse, ParseResult +except ImportError: # Python 2 + from urlparse import urlparse, ParseResult from django.conf import global_settings, settings from django.contrib.sites.models import Site, RequestSite @@ -46,15 +50,26 @@ class AuthViewsTestCase(TestCase): 'username': 'testclient', 'password': password, }) - self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith(settings.LOGIN_REDIRECT_URL)) self.assertTrue(SESSION_KEY in self.client.session) + return response def assertFormError(self, response, error): """Assert that error is found in response.context['form'] errors""" form_errors = list(itertools.chain(*response.context['form'].errors.values())) self.assertIn(force_text(error), form_errors) + def assertURLEqual(self, url, expected): + """ + Given two URLs, make sure all their components (the ones given by + urlparse) are equal, only comparing components that are present in both + URLs. + """ + fields = ParseResult._fields + + for attr, x, y in zip(fields, urlparse(url), urlparse(expected)): + if x and y and x != y: + self.fail("%r != %r (%s doesn't match)" % (url, expected, attr)) + @skipIfCustomUser class AuthViewNamedURLTests(AuthViewsTestCase): @@ -153,7 +168,6 @@ class PasswordResetTest(AuthViewsTestCase): def _test_confirm_start(self): # Start by creating the email response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) - self.assertEqual(response.status_code, 302) self.assertEqual(len(mail.outbox), 1) return self._read_signup_email(mail.outbox[0]) @@ -205,8 +219,6 @@ class PasswordResetTest(AuthViewsTestCase): url, path = self._test_confirm_start() response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'}) - # It redirects us to a 'complete' page: - self.assertEqual(response.status_code, 302) # Check the password has been changed u = User.objects.get(email='staffmember@example.com') self.assertTrue(u.check_password("anewpassword")) @@ -221,6 +233,47 @@ class PasswordResetTest(AuthViewsTestCase): 'new_password2': 'x'}) self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch']) + def test_reset_redirect_default(self): + response = self.client.post('/password_reset/', + {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/done/') + + def test_reset_custom_redirect(self): + response = self.client.post('/password_reset/custom_redirect/', + {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/custom/') + + def test_reset_custom_redirect_named(self): + response = self.client.post('/password_reset/custom_redirect/named/', + {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') + + def test_confirm_redirect_default(self): + url, path = self._test_confirm_start() + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/reset/done/') + + def test_confirm_redirect_custom(self): + url, path = self._test_confirm_start() + path = path.replace('/reset/', '/reset/custom/') + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/custom/') + + def test_confirm_redirect_custom_named(self): + url, path = self._test_confirm_start() + path = path.replace('/reset/', '/reset/custom/named/') + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') + @override_settings(AUTH_USER_MODEL='auth.CustomUser') class CustomUserPasswordResetTest(AuthViewsTestCase): @@ -285,8 +338,6 @@ class ChangePasswordTest(AuthViewsTestCase): 'new_password1': 'password1', 'new_password2': 'password1', }) - self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/password_change/done/')) self.fail_login() self.login(password='password1') @@ -298,13 +349,43 @@ class ChangePasswordTest(AuthViewsTestCase): 'new_password2': 'password1', }) self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/password_change/done/')) + self.assertURLEqual(response.url, '/password_change/done/') + @override_settings(LOGIN_URL='/login/') def test_password_change_done_fails(self): - with self.settings(LOGIN_URL='/login/'): - response = self.client.get('/password_change/done/') - self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/login/?next=/password_change/done/')) + response = self.client.get('/password_change/done/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/login/?next=/password_change/done/') + + def test_password_change_redirect_default(self): + self.login() + response = self.client.post('/password_change/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_change/done/') + + def test_password_change_redirect_custom(self): + self.login() + response = self.client.post('/password_change/custom/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/custom/') + + def test_password_change_redirect_custom_named(self): + self.login() + response = self.client.post('/password_change/custom/named/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') @skipIfCustomUser @@ -374,63 +455,68 @@ class LoginTest(AuthViewsTestCase): # the custom authentication form used by this login asserts # that a request is passed to the form successfully. + @skipIfCustomUser class LoginURLSettings(AuthViewsTestCase): - - def setUp(self): - super(LoginURLSettings, self).setUp() - self.old_LOGIN_URL = settings.LOGIN_URL - - def tearDown(self): - super(LoginURLSettings, self).tearDown() - settings.LOGIN_URL = self.old_LOGIN_URL - - def get_login_required_url(self, login_url): - settings.LOGIN_URL = login_url + """Tests for settings.LOGIN_URL.""" + def assertLoginURLEquals(self, url): response = self.client.get('/login_required/') self.assertEqual(response.status_code, 302) - return response.url + self.assertURLEqual(response.url, url) + @override_settings(LOGIN_URL='/login/') def test_standard_login_url(self): - login_url = '/login/' - login_required_url = self.get_login_required_url(login_url) - querystring = QueryDict('', mutable=True) - querystring['next'] = '/login_required/' - self.assertEqual(login_required_url, 'http://testserver%s?%s' % - (login_url, querystring.urlencode('/'))) + self.assertLoginURLEquals('/login/?next=/login_required/') + + @override_settings(LOGIN_URL='login') + def test_named_login_url(self): + self.assertLoginURLEquals('/login/?next=/login_required/') + @override_settings(LOGIN_URL='http://remote.example.com/login') def test_remote_login_url(self): - login_url = 'http://remote.example.com/login' - login_required_url = self.get_login_required_url(login_url) - querystring = QueryDict('', mutable=True) - querystring['next'] = 'http://testserver/login_required/' - self.assertEqual(login_required_url, - '%s?%s' % (login_url, querystring.urlencode('/'))) + quoted_next = urlquote('http://testserver/login_required/') + expected = 'http://remote.example.com/login?next=%s' % quoted_next + self.assertLoginURLEquals(expected) + @override_settings(LOGIN_URL='https:///login/') def test_https_login_url(self): - login_url = 'https:///login/' - login_required_url = self.get_login_required_url(login_url) - querystring = QueryDict('', mutable=True) - querystring['next'] = 'http://testserver/login_required/' - self.assertEqual(login_required_url, - '%s?%s' % (login_url, querystring.urlencode('/'))) + quoted_next = urlquote('http://testserver/login_required/') + expected = 'https:///login/?next=%s' % quoted_next + self.assertLoginURLEquals(expected) + @override_settings(LOGIN_URL='/login/?pretty=1') def test_login_url_with_querystring(self): - login_url = '/login/?pretty=1' - login_required_url = self.get_login_required_url(login_url) - querystring = QueryDict('pretty=1', mutable=True) - querystring['next'] = '/login_required/' - self.assertEqual(login_required_url, 'http://testserver/login/?%s' % - querystring.urlencode('/')) + self.assertLoginURLEquals('/login/?pretty=1&next=/login_required/') + @override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/') def test_remote_login_url_with_next_querystring(self): - login_url = 'http://remote.example.com/login/' - login_required_url = self.get_login_required_url('%s?next=/default/' % - login_url) - querystring = QueryDict('', mutable=True) - querystring['next'] = 'http://testserver/login_required/' - self.assertEqual(login_required_url, '%s?%s' % (login_url, - querystring.urlencode('/'))) + quoted_next = urlquote('http://testserver/login_required/') + expected = 'http://remote.example.com/login/?next=%s' % quoted_next + self.assertLoginURLEquals(expected) + + +@skipIfCustomUser +class LoginRedirectUrlTest(AuthViewsTestCase): + """Tests for settings.LOGIN_REDIRECT_URL.""" + def assertLoginRedirectURLEqual(self, url): + response = self.login() + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, url) + + def test_default(self): + self.assertLoginRedirectURLEqual('/accounts/profile/') + + @override_settings(LOGIN_REDIRECT_URL='/custom/') + def test_custom(self): + self.assertLoginRedirectURLEqual('/custom/') + + @override_settings(LOGIN_REDIRECT_URL='password_reset') + def test_named(self): + self.assertLoginRedirectURLEqual('/password_reset/') + + @override_settings(LOGIN_REDIRECT_URL='http://remote.example.com/welcome/') + def test_remote(self): + self.assertLoginRedirectURLEqual('http://remote.example.com/welcome/') @skipIfCustomUser @@ -457,11 +543,11 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/next_page/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/somewhere/')) + self.assertURLEqual(response.url, '/somewhere/') response = self.client.get('/logout/next_page/?next=/login/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/login/')) + self.assertURLEqual(response.url, '/login/') self.confirm_logged_out() @@ -470,7 +556,7 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/next_page/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/somewhere/')) + self.assertURLEqual(response.url, '/somewhere/') self.confirm_logged_out() def test_logout_with_redirect_argument(self): @@ -478,7 +564,7 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/?next=/login/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/login/')) + self.assertURLEqual(response.url, '/login/') self.confirm_logged_out() def test_logout_with_custom_redirect_argument(self): @@ -486,7 +572,15 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/custom_query/?follow=/somewhere/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/somewhere/')) + self.assertURLEqual(response.url, '/somewhere/') + self.confirm_logged_out() + + def test_logout_with_named_redirect(self): + "Logout resolves names or URLs passed as next_page." + self.login() + response = self.client.get('/logout/next_page/named/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') self.confirm_logged_out() def test_security_check(self, password='password'): diff --git a/django/contrib/auth/tests/urls.py b/django/contrib/auth/tests/urls.py index 51b05be648..835ff41de7 100644 --- a/django/contrib/auth/tests/urls.py +++ b/django/contrib/auth/tests/urls.py @@ -62,8 +62,19 @@ def custom_request_auth_login(request): urlpatterns = urlpatterns + patterns('', (r'^logout/custom_query/$', 'django.contrib.auth.views.logout', dict(redirect_field_name='follow')), (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')), + (r'^logout/next_page/named/$', 'django.contrib.auth.views.logout', dict(next_page='password_reset')), (r'^remote_user/$', remote_user_auth_view), (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), + (r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')), + (r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')), + (r'^reset/custom/(?P[0-9A-Za-z]{1,13})-(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + 'django.contrib.auth.views.password_reset_confirm', + dict(post_reset_redirect='/custom/')), + (r'^reset/custom/named/(?P[0-9A-Za-z]{1,13})-(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + 'django.contrib.auth.views.password_reset_confirm', + dict(post_reset_redirect='password_reset')), + (r'^password_change/custom/$', 'django.contrib.auth.views.password_change', dict(post_change_redirect='/custom/')), + (r'^password_change/custom/named/$', 'django.contrib.auth.views.password_change', dict(post_change_redirect='password_reset')), (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)), (r'^login_required/$', login_required(password_reset)), (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')), diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 8a554b0ad8..9691b31544 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -72,6 +72,9 @@ def logout(request, next_page=None, """ auth_logout(request) + if next_page is not None: + next_page = resolve_url(next_page) + if redirect_field_name in request.REQUEST: next_page = request.REQUEST[redirect_field_name] # Security check -- don't allow redirection to a different host. @@ -140,6 +143,8 @@ def password_reset(request, is_admin_site=False, extra_context=None): if post_reset_redirect is None: post_reset_redirect = reverse('django.contrib.auth.views.password_reset_done') + else: + post_reset_redirect = resolve_url(post_reset_redirect) if request.method == "POST": form = password_reset_form(request.POST) if form.is_valid(): @@ -193,6 +198,8 @@ def password_reset_confirm(request, uidb36=None, token=None, assert uidb36 is not None and token is not None # checked by URLconf if post_reset_redirect is None: post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') + else: + post_reset_redirect = resolve_url(post_reset_redirect) try: uid_int = base36_to_int(uidb36) user = UserModel._default_manager.get(pk=uid_int) @@ -243,6 +250,8 @@ def password_change(request, current_app=None, extra_context=None): if post_change_redirect is None: post_change_redirect = reverse('django.contrib.auth.views.password_change_done') + else: + post_change_redirect = resolve_url(post_change_redirect) if request.method == "POST": form = password_change_form(user=request.user, data=request.POST) if form.is_valid(): diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 6643fb7d32..a6244498f2 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -243,6 +243,12 @@ Minor features * The reason phrase can be customized in HTTP responses. +* 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. + Backwards incompatible changes in 1.6 ===================================== -- cgit v1.3 From a9b98f59aaff3f7281fedb27563a263dbb7753ec Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 19 May 2013 15:23:11 +0200 Subject: Clarified when triagers should close tickets as needsinfo. https://groups.google.com/d/msg/django-developers/dyldP9kFADc/rHTlRBVEP8MJ --- docs/internals/contributing/triaging-tickets.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index bc6148ca46..43b799ed51 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -349,8 +349,9 @@ Then, you can help out by: * Closing "Unreviewed" tickets as "invalid", "worksforme" or "duplicate." -* Closing "Unreviewed" tickets as "needsinfo" when they're feature requests - requiring a discussion on `django-developers`_. +* Closing "Unreviewed" tickets as "needsinfo" when the description is too + sparse to be actionnable, or when they're feature requests requiring a + discussion on `django-developers`_. * Correcting the "Needs tests", "Needs documentation", or "Has patch" flags for tickets where they are incorrectly set. -- 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') 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 41bea033b7e6b9d28c99f8c1ddedecad13009bd0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 19 May 2013 11:30:26 -0400 Subject: Fixed #20183 - Clarified docs assumption that certain objects exist in database. Thanks Tomasz Jaskowski for the patch. --- docs/topics/db/queries.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index a1cb5c79c5..39ef2b33fd 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -102,7 +102,8 @@ Saving ``ForeignKey`` and ``ManyToManyField`` fields Updating a :class:`~django.db.models.ForeignKey` field works exactly the same way as saving a normal field -- simply assign an object of the right type to the field in question. This example updates the ``blog`` attribute of an -``Entry`` instance ``entry``:: +``Entry`` instance ``entry``, assuming appropriate instances of ``Entry`` and +``Blog`` are already saved to the database (so we can retrieve them below):: >>> from blog.models import Entry >>> entry = Entry.objects.get(pk=1) -- cgit v1.3 From bdde7feb26f89e580c2b21154196fe3f8c774677 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sun, 19 May 2013 12:06:35 -0400 Subject: Added some links in /docs/intro/overview.txt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Claes Ström for the patch. --- docs/howto/custom-template-tags.txt | 2 ++ docs/intro/overview.txt | 40 +++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 17 deletions(-) (limited to 'docs') diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index f334c0f418..7611d44920 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -72,6 +72,8 @@ following: For more information on the :ttag:`load` tag, read its documentation. +.. _howto-writing-custom-template-filters: + Writing custom template filters ------------------------------- diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index 1a6c8fa19a..77838ffcaa 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -16,15 +16,17 @@ Design your model ================= Although you can use Django without a database, it comes with an -object-relational mapper in which you describe your database layout in Python +`object-relational mapper`_ in which you describe your database layout in Python code. +.. _object-relational mapper: http://en.wikipedia.org/wiki/Object-relational_mapping + The :doc:`data-model syntax ` offers many rich ways of representing your models -- so far, it's been solving two years' worth of database-schema problems. Here's a quick example, which might be saved in the file ``mysite/news/models.py``:: - from django.db import models + from django.db import models class Reporter(models.Model): full_name = models.CharField(max_length=70) @@ -57,8 +59,9 @@ tables in your database for whichever tables don't already exist. Enjoy the free API ================== -With that, you've got a free, and rich, :doc:`Python API ` to -access your data. The API is created on the fly, no code generation necessary: +With that, you've got a free, and rich, :doc:`Python API ` +to access your data. The API is created on the fly, no code generation +necessary: .. code-block:: python @@ -135,9 +138,9 @@ A dynamic admin interface: it's not just scaffolding -- it's the whole house ============================================================================ Once your models are defined, Django can automatically create a professional, -production ready :doc:`administrative interface ` -- a Web -site that lets authenticated users add, change and delete objects. It's as easy -as registering your model in the admin site:: +production ready :doc:`administrative interface ` -- +a Web site that lets authenticated users add, change and delete objects. It's +as easy as registering your model in the admin site:: # In models.py... @@ -173,9 +176,9 @@ application. Django encourages beautiful URL design and doesn't put any cruft in URLs, like ``.php`` or ``.asp``. To design URLs for an app, you create a Python module called a :doc:`URLconf -`. A table of contents for your app, it contains a simple mapping -between URL patterns and Python callback functions. URLconfs also serve to -decouple URLs from Python code. +`. A table of contents for your app, it contains a simple +mapping between URL patterns and Python callback functions. URLconfs also serve +to decouple URLs from Python code. Here's what a URLconf might look like for the ``Reporter``/``Article`` example above:: @@ -188,7 +191,7 @@ example above:: (r'^articles/(\d{4})/(\d{2})/(\d+)/$', 'news.views.article_detail'), ) -The code above maps URLs, as simple regular expressions, to the location of +The code above maps URLs, as simple `regular expressions`_, to the location of Python callback functions ("views"). The regular expressions use parenthesis to "capture" values from the URLs. When a user requests a page, Django runs through each pattern, in order, and stops at the first one that matches the @@ -196,6 +199,8 @@ requested URL. (If none of them matches, Django calls a special-case 404 view.) This is blazingly fast, because the regular expressions are compiled at load time. +.. _regular expressions: http://docs.python.org/2/howto/regex.html + Once one of the regexes matches, Django imports and calls the given view, which is a simple Python function. Each view gets passed a request object -- which contains request metadata -- and the values captured in the regex. @@ -216,7 +221,7 @@ Generally, a view retrieves data according to the parameters, loads a template and renders the template with the retrieved data. Here's an example view for ``year_archive`` from above:: - from django.shortcuts import render_to_response + from django.shortcuts import render_to_response def year_archive(request, year): a_list = Article.objects.filter(pub_date__year=year) @@ -233,8 +238,8 @@ The code above loads the ``news/year_archive.html`` template. Django has a template search path, which allows you to minimize redundancy among templates. In your Django settings, you specify a list of directories to check -for templates. If a template doesn't exist in the first directory, it checks the -second, and so on. +for templates with :setting:`TEMPLATE_DIRS`. If a template doesn't exist in the +first directory, it checks the second, and so on. Let's say the ``news/year_archive.html`` template was found. Here's what that might look like: @@ -265,9 +270,10 @@ character). This is called a template filter, and it's a way to filter the value of a variable. In this case, the date filter formats a Python datetime object in the given format (as found in PHP's date function). -You can chain together as many filters as you'd like. You can write custom -filters. You can write custom template tags, which run custom Python code behind -the scenes. +You can chain together as many filters as you'd like. You can write :ref:`custom +template filters `. You can write +:doc:`custom template tags `, which run custom +Python code behind the scenes. Finally, Django uses the concept of "template inheritance": That's what the ``{% extends "base.html" %}`` does. It means "First load the template called -- 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') 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') 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 6633eeb88634aa8c03c8737d2d23c96b96a37e0d Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 19 May 2013 17:55:12 +0200 Subject: Changed API to disable ATOMIC_REQUESTS per view. A decorator is easier to apply to CBVs. Backwards compatibility isn't an issue here, except for people running on a recent clone of master. Fixed a few minor problems in the transactions docs while I was there. --- django/core/handlers/base.py | 9 ++++--- django/db/transaction.py | 17 ++++++++++++ docs/topics/db/transactions.txt | 60 ++++++++++++++++++++++------------------- tests/handlers/views.py | 4 +-- 4 files changed, 57 insertions(+), 33 deletions(-) (limited to 'docs') diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index acc74db6f5..555fd98fc6 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -66,10 +66,11 @@ class BaseHandler(object): self._request_middleware = request_middleware def make_view_atomic(self, view): - if getattr(view, 'transactions_per_request', True): - for db in connections.all(): - if db.settings_dict['ATOMIC_REQUESTS']: - view = transaction.atomic(using=db.alias)(view) + non_atomic_requests = getattr(view, '_non_atomic_requests', set()) + for db in connections.all(): + if (db.settings_dict['ATOMIC_REQUESTS'] + and db.alias not in non_atomic_requests): + view = transaction.atomic(using=db.alias)(view) return view def get_response(self, request): diff --git a/django/db/transaction.py b/django/db/transaction.py index 48e7f900dd..f770f2efa7 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -333,6 +333,23 @@ def atomic(using=None, savepoint=True): return Atomic(using, savepoint) +def _non_atomic_requests(view, using): + try: + view._non_atomic_requests.add(using) + except AttributeError: + view._non_atomic_requests = set([using]) + return view + + +def non_atomic_requests(using=None): + if callable(using): + return _non_atomic_requests(using, DEFAULT_DB_ALIAS) + else: + if using is None: + using = DEFAULT_DB_ALIAS + return lambda view: _non_atomic_requests(view, using) + + ############################################ # Deprecated decorators / context managers # ############################################ diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 78786996cd..3193fe2c9a 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -45,14 +45,6 @@ You may perfom partial commits and rollbacks in your view code, typically with the :func:`atomic` context manager. However, at the end of the view, either all the changes will be committed, or none of them. -To disable this behavior for a specific view, you must set the -``transactions_per_request`` attribute of the view function itself to -``False``, like this:: - - def my_view(request): - do_stuff() - my_view.transactions_per_request = False - .. warning:: While the simplicity of this transaction model is appealing, it also makes it @@ -78,6 +70,26 @@ Note that only the execution of your view is enclosed in the transactions. Middleware runs outside of the transaction, and so does the rendering of template responses. +When :setting:`ATOMIC_REQUESTS ` is enabled, it's +still possible to prevent views from running in a transaction. + +.. function:: non_atomic_requests(using=None) + + This decorator will negate the effect of :setting:`ATOMIC_REQUESTS + ` for a given view:: + + from django.db import transaction + + @transaction.non_atomic_requests + def my_view(request): + do_stuff() + + @transaction.non_atomic_requests(using='other') + def my_other_view(request): + do_stuff_on_the_other_database() + + It only works if it's applied to the view itself. + .. versionchanged:: 1.6 Django used to provide this feature via ``TransactionMiddleware``, which is @@ -519,8 +531,8 @@ Transaction states ------------------ The three functions described above relied on a concept called "transaction -states". This mechanisme was deprecated in Django 1.6, but it's still -available until Django 1.8. +states". This mechanism was deprecated in Django 1.6, but it's still available +until Django 1.8. At any time, each database connection is in one of these two states: @@ -554,23 +566,14 @@ Transaction middleware In Django 1.6, ``TransactionMiddleware`` is deprecated and replaced :setting:`ATOMIC_REQUESTS `. While the general -behavior is the same, there are a few differences. - -With the transaction middleware, it was still possible to switch to autocommit -or to commit explicitly in a view. Since :func:`atomic` guarantees atomicity, -this isn't allowed any longer. +behavior is the same, there are two differences. -To avoid wrapping a particular view in a transaction, instead of:: - - @transaction.autocommit - def my_view(request): - do_stuff() - -you must now use this pattern:: - - def my_view(request): - do_stuff() - my_view.transactions_per_request = False +With the previous API, it was possible to switch to autocommit or to commit +explicitly anywhere inside a view. Since :setting:`ATOMIC_REQUESTS +` relies on :func:`atomic` which enforces atomicity, +this isn't allowed any longer. However, at the toplevel, it's still possible +to avoid wrapping an entire view in a transaction. To achieve this, decorate +the view with :func:`non_atomic_requests` instead of :func:`autocommit`. The transaction middleware applied not only to view functions, but also to middleware modules that came after it. For instance, if you used the session @@ -624,6 +627,9 @@ you should now use:: finally: transaction.set_autocommit(False) +Unless you're implementing a transaction management framework, you shouldn't +ever need to do this. + Disabling transaction management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -653,7 +659,7 @@ Sequences of custom SQL queries If you're executing several :ref:`custom SQL queries ` in a row, each one now runs in its own transaction, instead of sharing the same "automatic transaction". If you need to enforce atomicity, you must wrap -the sequence of queries in :func:`commit_on_success`. +the sequence of queries in :func:`atomic`. To check for this problem, look for calls to ``cursor.execute()``. They're usually followed by a call to ``transaction.commit_unless_managed()``, which diff --git a/tests/handlers/views.py b/tests/handlers/views.py index 22d9ea4c7d..9cc86ae6f3 100644 --- a/tests/handlers/views.py +++ b/tests/handlers/views.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.db import connection +from django.db import connection, transaction from django.http import HttpResponse, StreamingHttpResponse def regular(request): @@ -12,6 +12,6 @@ def streaming(request): def in_transaction(request): return HttpResponse(str(connection.in_atomic_block)) +@transaction.non_atomic_requests def not_in_transaction(request): return HttpResponse(str(connection.in_atomic_block)) -not_in_transaction.transactions_per_request = False -- 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') 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') 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 4280217f31fc634d320b0cf30bcb6d582b19d784 Mon Sep 17 00:00:00 2001 From: Ryan Kaskel Date: Mon, 20 May 2013 12:13:03 -0400 Subject: Fixed #20403 -- Ignore forms marked for deletion when validating max_num. --- django/forms/formsets.py | 3 ++- docs/topics/forms/formsets.txt | 3 ++- tests/forms_tests/tests/test_formsets.py | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/django/forms/formsets.py b/django/forms/formsets.py index 3ec94d20ec..fd98c43405 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -303,7 +303,8 @@ class BaseFormSet(object): form = self.forms[i] self._errors.append(form.errors) try: - if (self.validate_max and self.total_form_count() > self.max_num) or \ + if (self.validate_max and + self.total_form_count() - len(self.deleted_forms) > self.max_num) or \ self.management_form.cleaned_data[TOTAL_FORM_COUNT] > self.absolute_max: raise ValidationError(ungettext( "Please submit %d or fewer forms.", diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index e55c22e3a2..68551d380b 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -283,7 +283,8 @@ Validating the number of forms in a formset 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``. +that the number of forms in the data set, minus those marked for +deletion, is less than or equal to ``max_num``. >>> from django.forms.formsets import formset_factory >>> from myapp.forms import ArticleForm diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index 31adb921ba..787f177677 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -986,6 +986,24 @@ class FormsFormsetTestCase(TestCase): self.assertEqual(list(formset.non_form_errors()), ['This is a non-form error']) + def test_validate_max_ignores_forms_marked_for_deletion(self): + class CheckForm(Form): + field = IntegerField() + + data = { + 'check-TOTAL_FORMS': '2', + 'check-INITIAL_FORMS': '0', + 'check-MAX_NUM_FORMS': '1', + 'check-0-field': '200', + 'check-0-DELETE': '', + 'check-1-field': '50', + 'check-1-DELETE': 'on', + } + CheckFormSet = formset_factory(CheckForm, max_num=1, validate_max=True, + can_delete=True) + formset = CheckFormSet(data, prefix='check') + self.assertTrue(formset.is_valid()) + data = { 'choices-TOTAL_FORMS': '1', # the number of forms rendered -- cgit v1.3 From 490672f057cf1660606eeb14ef7f833ecff3df32 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 20 May 2013 13:45:32 -0400 Subject: Tweaked unit test 'quick start' explanation. Thanks Jeremy Dunck. --- .../contributing/writing-code/unit-tests.txt | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) (limited to 'docs') diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index f56bf1cdeb..0737b84888 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -27,15 +27,13 @@ Quickstart Running the tests requires a Django settings module that defines the databases to use. To make it easy to get started, Django provides a sample settings module that uses the SQLite database. To run the tests -with this sample ``settings`` module, ``cd`` into the Django -``tests/`` directory and run: +with this sample ``settings`` module: .. code-block:: bash - ./runtests.py --settings=test_sqlite - -If you get an ``ImportError: No module named django.contrib`` error, -you need to add your install of Django to your ``PYTHONPATH``. + git clone git@github.com:django/django.git django-repo + cd django-repo/tests + PYTHONPATH=..:$PYTHONPATH python ./runtests.py --settings=test_sqlite .. _running-unit-tests-settings: @@ -47,14 +45,10 @@ SQLite. If you want to test behavior using a different database (and if you're proposing patches for Django, it's a good idea to test across databases), you may need to define your own settings file. -To run the tests with different settings, ``cd`` to the ``tests/`` directory -and type: - -.. code-block:: bash - - ./runtests.py --settings=path.to.django.settings +To run the tests with different settings, ensure that the module is on your +``PYTHONPATH`` and pass the module with ``--settings``. -The :setting:`DATABASES` setting in this test settings module needs to define +The :setting:`DATABASES` setting in any test settings module needs to define two databases: * A ``default`` database. This database should use the backend that -- cgit v1.3 From 2fd61285d109a04bf22d1822e834cbb7bcf4239f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 20 May 2013 13:56:19 -0400 Subject: Fixed #20471 - Typos in topics/forms/formsets.txt --- docs/topics/forms/formsets.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 68551d380b..8745e9761d 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -58,7 +58,7 @@ example:: >>> import datetime >>> from django.forms.formsets import formset_factory - >>> from myapp.forms imporrt ArticleForm + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, extra=2) >>> formset = ArticleFormSet(initial=[ ... {'title': u'Django is now open source', @@ -92,7 +92,7 @@ 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 + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1) >>> formset = ArticleFormSet() >>> for form in formset: @@ -130,7 +130,7 @@ 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 + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm) >>> data = { ... 'form-TOTAL_FORMS': u'1', -- cgit v1.3 From 4ba1c2e785feecfa7a47aa5336a2b595f086a765 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Mon, 15 Apr 2013 10:54:37 -0300 Subject: Fixed #9321 -- Deprecated hard-coding of help text in model ManyToManyField fields. This is backward incompatible for custom form field/widgets that rely on the hard-coded 'Hold down "Control", or "Command" on a Mac, to select more than one.' sentence. Application that use standard model form fields and widgets aren't affected but need to start handling these help texts by themselves before Django 1.8. For more details, see the related release notes and deprecation timeline sections added with this commit. --- django/db/models/fields/related.py | 5 +--- django/forms/models.py | 5 +++- docs/internals/deprecation.txt | 5 ++++ docs/releases/1.6.txt | 51 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 5 deletions(-) (limited to 'docs') diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 256c3e0f61..fd0b24433f 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -11,7 +11,7 @@ from django.db.models.deletion import CASCADE from django.utils.encoding import smart_text from django.utils import six from django.utils.deprecation import RenameMethodsBase -from django.utils.translation import ugettext_lazy as _, string_concat +from django.utils.translation import ugettext_lazy as _ from django.utils.functional import curry, cached_property from django.core import exceptions from django import forms @@ -1348,9 +1348,6 @@ class ManyToManyField(RelatedField): super(ManyToManyField, self).__init__(**kwargs) - msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') - self.help_text = string_concat(self.help_text, ' ', msg) - def _get_path_info(self, direct=False): """ Called by both direct an indirect m2m traversal. diff --git a/django/forms/models.py b/django/forms/models.py index 93c8b89efe..68c1341cf7 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -18,7 +18,7 @@ from django.utils.encoding import smart_text, force_text from django.utils.datastructures import SortedDict from django.utils import six from django.utils.text import get_text_list, capfirst -from django.utils.translation import ugettext_lazy as _, ugettext +from django.utils.translation import ugettext_lazy as _, ugettext, string_concat __all__ = ( @@ -1104,6 +1104,9 @@ class ModelMultipleChoiceField(ModelChoiceField): super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices, required, widget, label, initial, help_text, *args, **kwargs) + if isinstance(self.widget, SelectMultiple): + msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') + self.help_text = string_concat(self.help_text, ' ', msg) if self.help_text else msg def clean(self, value): if self.required and not value: diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 095b6d0a33..5fa7ea16ac 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -392,6 +392,11 @@ these changes. * The ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting will be removed. +* Usage of the hard-coded *Hold down "Control", or "Command" on a Mac, to select + more than one.* string to override or append to user-provided ``help_text`` in + forms for ManyToMany model fields will not be performed by Django anymore + either at the model or forms layer. + 2.0 --- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 9950717420..a417e81f62 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -481,6 +481,44 @@ parameters. For example:: ``SQLite`` users need to check and update such queries. +.. _m2m-help_text: + +Help text of model form fields for ManyToManyField fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +HTML rendering of model form fields corresponding to +:class:`~django.db.models.ManyToManyField` ORM model fields used to get the +hard-coded sentence + + *Hold down "Control", or "Command" on a Mac, to select more than one.* + +(or its translation to the active locale) imposed as the help legend shown along +them if neither :attr:`model ` nor :attr:`form +` ``help_text`` attribute was specified by the +user (or appended to, if ``help_text`` was provided.) + +This happened always, possibly even with form fields implementing user +interactions that don't involve a keyboard and/or a mouse and was handled at the +model field layer. + +Starting with Django 1.6 this doesn't happen anymore. + +The change can affect you in a backward incompatible way if you employ custom +model form fields and/or widgets for ``ManyToManyField`` model fields whose UIs +do rely on the automatic provision of the mentioned hard-coded sentence. These +form field implementations need to adapt to the new scenario by providing their +own handling of the ``help_text`` attribute. + +Applications that use Django :doc:`model form ` +facilities together with Django built-in form :doc:`fields ` +and :doc:`widgets ` aren't affected but need to be aware of +what's described in :ref:`m2m-help_text-deprecation` below. + +This is because, as an temporary backward-compatible provision, the described +non-standard behavior has been preserved but moved to the model form field layer +and occurs only when the associated widget is +:class:`~django.forms.SelectMultiple` or a subclass. + Miscellaneous ~~~~~~~~~~~~~ @@ -707,3 +745,16 @@ you can set set the ``form_class`` attribute to a ``ModelForm`` that explicitly defines the fields to be used. Defining an ``UpdateView`` or ``CreateView`` subclass to be used with a model but without an explicit list of fields is deprecated. + +.. _m2m-help_text-deprecation: + +Munging of help text of model form fields for ManyToManyField fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All special handling of the ``help_text`` attibute 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. + +Help text of these fields will need to be handled either by applications, custom +form fields or widgets, just like happens with the rest of the model field +types. -- 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') 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') 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') 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') 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 70679243d1786e03557c28929f9762a119e3ac14 Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Thu, 2 Aug 2012 00:09:26 +0300 Subject: Fixed #18702 -- Removed chunked reads from QuerySet iteration --- django/db/models/query.py | 149 +++++++++------------------------------------- docs/releases/1.6.txt | 19 ++++++ tests/queries/tests.py | 44 +++----------- 3 files changed, 56 insertions(+), 156 deletions(-) (limited to 'docs') diff --git a/django/db/models/query.py b/django/db/models/query.py index f2015f57a8..cc07ecbfd0 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -20,11 +20,6 @@ from django.utils.functional import partition from django.utils import six from django.utils import timezone -# Used to control how many objects are worked with at once in some cases (e.g. -# when deleting objects). -CHUNK_SIZE = 100 -ITER_CHUNK_SIZE = CHUNK_SIZE - # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 @@ -41,7 +36,6 @@ class QuerySet(object): self._db = using self.query = query or sql.Query(self.model) self._result_cache = None - self._iter = None self._sticky_filter = False self._for_write = False self._prefetch_related_lookups = [] @@ -57,8 +51,8 @@ class QuerySet(object): Deep copy of a QuerySet doesn't populate the cache """ obj = self.__class__() - for k,v in self.__dict__.items(): - if k in ('_iter','_result_cache'): + for k, v in self.__dict__.items(): + if k == '_result_cache': obj.__dict__[k] = None else: obj.__dict__[k] = copy.deepcopy(v, memo) @@ -69,10 +63,8 @@ class QuerySet(object): Allows the QuerySet to be pickled. """ # Force the cache to be fully populated. - len(self) - + self._fetch_all() obj_dict = self.__dict__.copy() - obj_dict['_iter'] = None return obj_dict def __repr__(self): @@ -82,95 +74,31 @@ class QuerySet(object): return repr(data) def __len__(self): - # Since __len__ is called quite frequently (for example, as part of - # list(qs), we make some effort here to be as efficient as possible - # whilst not messing up any existing iterators against the QuerySet. - if self._result_cache is None: - if self._iter: - self._result_cache = list(self._iter) - else: - self._result_cache = list(self.iterator()) - elif self._iter: - self._result_cache.extend(self._iter) - if self._prefetch_related_lookups and not self._prefetch_done: - self._prefetch_related_objects() + self._fetch_all() return len(self._result_cache) def __iter__(self): - if self._prefetch_related_lookups and not self._prefetch_done: - # We need all the results in order to be able to do the prefetch - # in one go. To minimize code duplication, we use the __len__ - # code path which also forces this, and also does the prefetch - len(self) - - if self._result_cache is None: - self._iter = self.iterator() - self._result_cache = [] - if self._iter: - return self._result_iter() - # Python's list iterator is better than our version when we're just - # iterating over the cache. + """ + The queryset iterator protocol uses three nested iterators in the + default case: + 1. sql.compiler:execute_sql() + - Returns 100 rows at time (constants.GET_ITERATOR_CHUNK_SIZE) + using cursor.fetchmany(). This part is responsible for + doing some column masking, and returning the rows in chunks. + 2. sql/compiler.results_iter() + - Returns one row at time. At this point the rows are still just + tuples. In some cases the return values are converted to + Python values at this location (see resolve_columns(), + resolve_aggregate()). + 3. self.iterator() + - Responsible for turning the rows into model objects. + """ + self._fetch_all() return iter(self._result_cache) - def _result_iter(self): - pos = 0 - while 1: - upper = len(self._result_cache) - while pos < upper: - yield self._result_cache[pos] - pos = pos + 1 - if not self._iter: - raise StopIteration - if len(self._result_cache) <= pos: - self._fill_cache() - - def __bool__(self): - if self._prefetch_related_lookups and not self._prefetch_done: - # We need all the results in order to be able to do the prefetch - # in one go. To minimize code duplication, we use the __len__ - # code path which also forces this, and also does the prefetch - len(self) - - if self._result_cache is not None: - return bool(self._result_cache) - try: - next(iter(self)) - except StopIteration: - return False - return True - - def __nonzero__(self): # Python 2 compatibility - return type(self).__bool__(self) - - def __contains__(self, val): - # The 'in' operator works without this method, due to __iter__. This - # implementation exists only to shortcut the creation of Model - # instances, by bailing out early if we find a matching element. - pos = 0 - if self._result_cache is not None: - if val in self._result_cache: - return True - elif self._iter is None: - # iterator is exhausted, so we have our answer - return False - # remember not to check these again: - pos = len(self._result_cache) - else: - # We need to start filling the result cache out. The following - # ensures that self._iter is not None and self._result_cache is not - # None - it = iter(self) - - # Carry on, one result at a time. - while True: - if len(self._result_cache) <= pos: - self._fill_cache(num=1) - if self._iter is None: - # we ran out of items - return False - if self._result_cache[pos] == val: - return True - pos += 1 + def __nonzero__(self): + self._fetch_all() + return bool(self._result_cache) def __getitem__(self, k): """ @@ -184,19 +112,6 @@ class QuerySet(object): "Negative indexing is not supported." if self._result_cache is not None: - if self._iter is not None: - # The result cache has only been partially populated, so we may - # need to fill it out a bit more. - if isinstance(k, slice): - if k.stop is not None: - # Some people insist on passing in strings here. - bound = int(k.stop) - else: - bound = None - else: - bound = k + 1 - if len(self._result_cache) < bound: - self._fill_cache(bound - len(self._result_cache)) return self._result_cache[k] if isinstance(k, slice): @@ -370,7 +285,7 @@ class QuerySet(object): If the QuerySet is already fully cached this simply returns the length of the cached results set to avoid multiple SELECT COUNT(*) calls. """ - if self._result_cache is not None and not self._iter: + if self._result_cache is not None: return len(self._result_cache) return self.query.get_count(using=self.db) @@ -933,17 +848,11 @@ class QuerySet(object): c._setup_query() return c - def _fill_cache(self, num=None): - """ - Fills the result cache with 'num' more entries (or until the results - iterator is exhausted). - """ - if self._iter: - try: - for i in range(num or ITER_CHUNK_SIZE): - self._result_cache.append(next(self._iter)) - except StopIteration: - self._iter = None + def _fetch_all(self): + if self._result_cache is None: + self._result_cache = list(self.iterator()) + if self._prefetch_related_lookups and not self._prefetch_done: + self._prefetch_related_objects() def _next_is_sticky(self): """ diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 29b4569d4e..a1a29471a3 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -524,6 +524,25 @@ non-standard behavior has been preserved but moved to the model form field layer and occurs only when the associated widget is :class:`~django.forms.SelectMultiple` or a subclass. +QuerySet iteration +~~~~~~~~~~~~~~~~~~ + +The ``QuerySet`` iteration was changed to immediately convert all fetched +rows to ``Model`` objects. In Django 1.5 and earlier the fetched rows were +converted to ``Model`` objects in chunks of 100. + +Existing code will work, but the amount of rows converted to objects +might change in certain use cases. Such usages include partially looping +over a queryset or any usage which ends up doing ``__bool__`` or +``__contains__``. + +Notably most database backends did fetch all the rows in one go already in +1.5. + +It is still possible to convert the fetched rows to ``Model`` objects +lazily by using the :meth:`~django.db.models.query.QuerySet.iterator()` +method. + Miscellaneous ~~~~~~~~~~~~~ diff --git a/tests/queries/tests.py b/tests/queries/tests.py index ad5dde34b5..ffaf6435ec 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -9,7 +9,6 @@ from django.conf import settings from django.core.exceptions import FieldError from django.db import DatabaseError, connection, connections, DEFAULT_DB_ALIAS from django.db.models import Count, F, Q -from django.db.models.query import ITER_CHUNK_SIZE from django.db.models.sql.where import WhereNode, EverythingNode, NothingNode from django.db.models.sql.datastructures import EmptyResultSet from django.test import TestCase, skipUnlessDBFeature @@ -1211,16 +1210,6 @@ class Queries2Tests(TestCase): ordered=False ) - def test_ticket7411(self): - # Saving to db must work even with partially read result set in another - # cursor. - for num in range(2 * ITER_CHUNK_SIZE + 1): - _ = Number.objects.create(num=num) - - for i, obj in enumerate(Number.objects.all()): - obj.save() - if i > 10: break - def test_ticket7759(self): # Count should work with a partially read result set. count = Number.objects.count() @@ -1700,31 +1689,6 @@ class Queries6Tests(TestCase): ann1.notes.add(n1) ann2 = Annotation.objects.create(name='a2', tag=t4) - # This next test used to cause really weird PostgreSQL behavior, but it was - # only apparent much later when the full test suite ran. - # - Yeah, it leaves global ITER_CHUNK_SIZE to 2 instead of 100... - #@unittest.expectedFailure - def test_slicing_and_cache_interaction(self): - # We can do slicing beyond what is currently in the result cache, - # too. - - # We need to mess with the implementation internals a bit here to decrease the - # cache fill size so that we don't read all the results at once. - from django.db.models import query - query.ITER_CHUNK_SIZE = 2 - qs = Tag.objects.all() - - # Fill the cache with the first chunk. - self.assertTrue(bool(qs)) - self.assertEqual(len(qs._result_cache), 2) - - # Query beyond the end of the cache and check that it is filled out as required. - self.assertEqual(repr(qs[4]), '') - self.assertEqual(len(qs._result_cache), 5) - - # But querying beyond the end of the result set will fail. - self.assertRaises(IndexError, lambda: qs[100]) - def test_parallel_iterators(self): # Test that parallel iterators work. qs = Tag.objects.all() @@ -2533,6 +2497,14 @@ class WhereNodeTest(TestCase): w = WhereNode(children=[empty_w, NothingNode()], connector='OR') self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) + +class IteratorExceptionsTest(TestCase): + def test_iter_exceptions(self): + qs = ExtraInfo.objects.only('author') + with self.assertRaises(AttributeError): + list(qs) + + class NullJoinPromotionOrTest(TestCase): def setUp(self): self.d1 = ModelD.objects.create(name='foo') -- 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') 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') 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') 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 ef8a9bc7fafc394f2c413a6ad4057d67134d742b Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 22 May 2013 07:57:36 -0400 Subject: Fixed #17048 - Added docs for upgrading Django. Thanks Donald Stufft for the suggestion and Susan Tan and Brian Fabian Crain for the patch. --- AUTHORS | 1 + docs/howto/index.txt | 1 + docs/howto/upgrade-version.txt | 91 ++++++++++++++++++++++++++++++++++++++++++ docs/releases/index.txt | 9 +++-- 4 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 docs/howto/upgrade-version.txt (limited to 'docs') diff --git a/AUTHORS b/AUTHORS index 564284598c..e963c0914f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -159,6 +159,7 @@ answer newbie questions, and generally made Django that much better: Paul Collins Robert Coup Deric Crago + Brian Fabian Crain David Cramer Pete Crosier Matt Croydon diff --git a/docs/howto/index.txt b/docs/howto/index.txt index 9d5b067a82..fe25c11756 100644 --- a/docs/howto/index.txt +++ b/docs/howto/index.txt @@ -15,6 +15,7 @@ you quickly accomplish common tasks. custom-template-tags custom-file-storage deployment/index + upgrade-version error-reporting initial-data jython diff --git a/docs/howto/upgrade-version.txt b/docs/howto/upgrade-version.txt new file mode 100644 index 0000000000..d07f9ad07d --- /dev/null +++ b/docs/howto/upgrade-version.txt @@ -0,0 +1,91 @@ +=================================== +Upgrading Django to a newer version +=================================== + +While it can be a complex process at times, upgrading to the latest Django +version has several benefits: + +* New features and improvements are added. +* Bugs are fixed. +* Older version of Django will eventually no longer receive security updates. + (see :ref:`backwards-compatibility-policy`). +* Upgrading as each new Django release is available makes future upgrades less + painful by keeping your code base up to date. + +Here are some things to consider to help make your upgrade process as smooth as +possible. + +Required Reading +================ + +If it's your first time doing an upgrade, it is useful to read the :doc:`guide +on the different release processes `. + +Afterwards, you should familiarize yourself with the changes that were made in +the new Django version(s): + +* Read the :doc:`release notes ` for each 'final' release from + the one after your current Django version, up to and including the version to + which you plan to upgrade. +* Look at the :doc:`deprecation timeline` for the + relevant versions. + +Pay particular attention to backwards incompatible changes to get a clear idea +of what will be needed for a successful upgrade. + +Dependencies +============ + +In most cases it will be necessary to upgrade to the latest version of your +Django-related dependencies as well. If the Django version was recently +released or if some of your dependencies are not well-maintained, some of your +dependencies may not yet support the new Django version. In these cases you may +have to wait until new versions of your dependencies are released. + +Installation +============ + +Once you're ready, it is time to :doc:`install the new Django version +`. If you are using virtualenv_ and it is a major upgrade, you +might want to set up a new environment will all the dependencies first. + +Exactly which steps you will need to take depends on your installation process. +The most convenient way is to use pip_: + +.. code-block:: bash + + pip install Django + +pip_ also automatically uninstalls the previous version of Django. + +If you use some other installation process, you might have to manually +:ref:`uninstall the old Django version ` and +should look at the complete installation instructions. + +.. _pip: http://www.pip-installer.org/ +.. _virtualenv: http://www.virtualenv.org/ + +Testing +======= + +When the new environment is set up, :doc:`run the full test suite +` for your application. In Python 2.7+, deprecation +warnings are silenced by default. It is useful to turn the warnings on so they +are shown in the test output (you can also use the flag if you test your app +manually using ``manage.py runserver``): + +.. code-block:: bash + + python -Wall manage.py test + +After you have run the tests, fix any failures. While you have the release +notes fresh in your mind, it may also be a good time to take advantage of new +features in Django by refactoring your code to eliminate any deprecation +warnings. + +Deployment +========== + +When you are sufficiently confident your app works with the new version of +Django, you're ready to go ahead and :doc:`deploy ` +your upgraded Django project. diff --git a/docs/releases/index.txt b/docs/releases/index.txt index c5afd8c719..85b3d211a8 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -6,10 +6,11 @@ Release notes for the official Django releases. Each release note will tell you what's new in each version, and will also describe any backwards-incompatible changes made in that version. -For those upgrading to a new version of Django, you will need to check -all the backwards-incompatible changes and deprecated features for -each 'final' release from the one after your current Django version, -up to and including the new version. +For those :doc:`upgrading to a new version of Django`, +you will need to check all the backwards-incompatible changes and +:doc:`deprecated features` for each 'final' release +from the one after your current Django version, up to and including the new +version. Final releases ============== -- cgit v1.3 From 0e45f75112f3c540476207fa16bdbaec14872f6c Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 22 May 2013 09:39:46 -0400 Subject: Added a note about pip upgrade flag. Thanks Jannis Leidel for the suggestion. --- docs/howto/upgrade-version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/howto/upgrade-version.txt b/docs/howto/upgrade-version.txt index d07f9ad07d..e6b80f346f 100644 --- a/docs/howto/upgrade-version.txt +++ b/docs/howto/upgrade-version.txt @@ -54,7 +54,7 @@ The most convenient way is to use pip_: .. code-block:: bash - pip install Django + pip install -U Django pip_ also automatically uninstalls the previous version of Django. -- cgit v1.3 From 2699219c233637d0086718bd58a0b693bd652bf2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 22 May 2013 10:25:49 -0400 Subject: Added some text I meant to add in the last commit. --- docs/howto/upgrade-version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/howto/upgrade-version.txt b/docs/howto/upgrade-version.txt index e6b80f346f..8777f433f9 100644 --- a/docs/howto/upgrade-version.txt +++ b/docs/howto/upgrade-version.txt @@ -50,7 +50,7 @@ Once you're ready, it is time to :doc:`install the new Django version might want to set up a new environment will all the dependencies first. Exactly which steps you will need to take depends on your installation process. -The most convenient way is to use pip_: +The most convenient way is to use pip_ with the ``--upgrade`` or ``-U`` flag: .. code-block:: bash -- cgit v1.3 From 01948e384f5508c126c7216e43db3654bf6330f0 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 22 May 2013 10:22:32 -0400 Subject: Clarified policy for stable branches. Thanks Ramiro Morales for the initial patch and Preston Holmes for the review. --- docs/internals/git.txt | 76 ++++++++++++++++++++++---------------- docs/internals/release-process.txt | 76 ++++++++++++++++++-------------------- 2 files changed, 81 insertions(+), 71 deletions(-) (limited to 'docs') diff --git a/docs/internals/git.txt b/docs/internals/git.txt index 2b1a279d89..3904ff83d4 100644 --- a/docs/internals/git.txt +++ b/docs/internals/git.txt @@ -35,8 +35,9 @@ The Git repository includes several `branches`_: the next packaged release of Django. This is where most development activity is focused. -* ``stable/A.B.x`` are the maintenance branches. They are used to support - older versions of Django. +* ``stable/A.B.x`` are the branches where release preparation work happens. + They are also used for support and bugfix releases which occur as necessary + after the initial release of a major or minor version. * ``soc20XX/`` branches were used by students who worked on Django during the 2009 and 2010 Google Summer of Code programs. @@ -83,13 +84,50 @@ coding style and how to generate and submit a patch. Other branches ============== -Django uses branches for two main purposes: +Django uses branches to prepare for releases of Django (whether they be +:term:`major `, :term:`minor `, or +:term:`micro `). -1. Development of major or experimental features, to keep them from - affecting progress on other work in master. +In the past when Django was hosted on Subversion, branches were also used for +feature development. Now Django is hosted on Git and feature development is +done on contributor's forks, but the Subversion feature branches remain in Git +for historical reference. -2. Security and bugfix support for older releases of Django, during - their support lifetimes. +Stable branches +--------------- + +These branches can be found in the repository as ``stable/A.B.x`` +branches and will be created right after the first alpha is tagged. + +For example, immediately after *Django 1.5 alpha 1* was tagged, the branch +``stable/1.5.x`` was created and all further work on preparing the code for the +final 1.5 release was done there. + +These branches also provide limited bugfix support for the most recent released +version of Django and security support for the two most recently-released +versions of Django. + +For example, after the release of Django 1.5, the branch ``stable/1.5.x`` +receives only fixes for security and critical stability bugs, which are +eventually released as Django 1.5.1 and so on, ``stable/1.4.x`` receives only +security fixes, and ``stable/1.3.x`` no longer receives any updates. + +.. admonition:: Historical information + + This policy for handling ``stable/A.B.x`` branches was adopted starting + with the Django 1.5 release cycle. + + Previously, these branches weren't created until right after the releases + and the stabilization work occurred on the main repository branch. Thus, + no new features development work for the next release of Django could be + committed until the final release happened. + + For example, shortly after the release of Django 1.3 the branch + ``stable/1.3.x`` was created. Official support for that release has expired, + and so it no longer receives direct maintenance from the Django project. + However, that and all other similarly named branches continue to exist and + interested community members have occasionally used them to provide + unofficial support for old Django releases. Feature-development branches ---------------------------- @@ -203,30 +241,6 @@ All of the above-mentioned branches now reside in ``attic``. Finally, the repository contains ``soc2009/xxx`` and ``soc2010/xxx`` feature branches, used for Google Summer of Code projects. -Support and bugfix branches ---------------------------- - -In addition to fixing bugs in current master, the Django project provides -official bugfix support for the most recent released version of Django, and -security support for the two most recently-released versions of Django. - -This support is provided via branches in which the necessary bug or security -fixes are applied; the branches are then used as the basis for issuing bugfix -or security releases. - -These branches can be found in the repository as ``stable/A.B.x`` -branches, and new branches will be created there after each new Django -release. - -For example, shortly after the release of Django 1.0, the branch -``stable/1.0.x`` was created to receive bug fixes, and shortly after the -release of Django 1.1 the branch ``stable/1.1.x`` was created. - -Official support for the above mentioned releases has expired, and so they no -longer receive direct maintenance from the Django project. However, the -branches continue to exist and interested community members have occasionally -used them to provide unofficial support for old Django releases. - Tags ==== diff --git a/docs/internals/release-process.txt b/docs/internals/release-process.txt index 29ce3914b4..2003e79079 100644 --- a/docs/internals/release-process.txt +++ b/docs/internals/release-process.txt @@ -39,49 +39,45 @@ issued from those branches. For more information about how the Django project issues new releases for security purposes, please see :doc:`our security policies `. -Major releases --------------- +.. glossary:: -Major releases (1.0, 2.0, etc.) will happen very infrequently (think "years", -not "months"), and may represent major, sweeping changes to Django. + Major release + Major releases (1.0, 2.0, etc.) will happen very infrequently (think "years", + not "months"), and may represent major, sweeping changes to Django. -Minor releases --------------- + Minor release + Minor release (1.5, 1.6, etc.) will happen roughly every nine months -- see + `release process`_, below for details. These releases will contain new + features, improvements to existing features, and such. -Minor release (1.5, 1.6, etc.) will happen roughly every nine months -- see -`release process`_, below for details. These releases will contain new -features, improvements to existing features, and such. + .. _internal-release-deprecation-policy: -.. _internal-release-deprecation-policy: + A minor release may deprecate certain features from previous releases. If a + feature is deprecated in version ``A.B``, it will continue to work in versions + ``A.B`` and ``A.B+1`` but raise warnings. It will be removed in version + ``A.B+2``. -A minor release may deprecate certain features from previous releases. If a -feature is deprecated in version ``A.B``, it will continue to work in versions -``A.B`` and ``A.B+1`` but raise warnings. It will be removed in version -``A.B+2``. + So, for example, if we decided to start the deprecation of a function in + Django 1.5: -So, for example, if we decided to start the deprecation of a function in -Django 1.5: + * Django 1.5 will contain a backwards-compatible replica of the function which + will raise a ``PendingDeprecationWarning``. This warning is silent by + default; you can turn on display of these warnings with the ``-Wd`` option + of Python. -* Django 1.5 will contain a backwards-compatible replica of the function which - will raise a ``PendingDeprecationWarning``. This warning is silent by - default; you can turn on display of these warnings with the ``-Wd`` option - of Python. + * Django 1.6 will contain the backwards-compatible replica, but the warning + will be promoted to a full-fledged ``DeprecationWarning``. This warning is + *loud* by default, and will likely be quite annoying. -* Django 1.6 will contain the backwards-compatible replica, but the warning - will be promoted to a full-fledged ``DeprecationWarning``. This warning is - *loud* by default, and will likely be quite annoying. + * Django 1.7 will remove the feature outright. -* Django 1.7 will remove the feature outright. + Micro release + Micro releases (1.5.1, 1.6.2, 1.6.1, etc.) will be issued as needed, often to + fix security issues. -Micro releases --------------- - -Micro releases (1.5.1, 1.6.2, 1.6.1, etc.) will be issued as needed, often to -fix security issues. - -These releases will be 100% compatible with the associated minor release, unless -this is impossible for security reasons. So the answer to "should I upgrade to -the latest micro release?" will always be "yes." + These releases will be 100% compatible with the associated minor release, unless + this is impossible for security reasons. So the answer to "should I upgrade to + the latest micro release?" will always be "yes." .. _backwards-compatibility-policy: @@ -126,15 +122,15 @@ Django 1.6 and 1.7. At this point in time: * Features will be added to development master, to be released as Django 1.7. -* Critical bug fixes will be applied to the ``stable/1.6.X`` branch, and +* Critical bug fixes will be applied to the ``stable/1.6.x`` branch, and released as 1.6.1, 1.6.2, etc. -* Security fixes will be applied to ``master``, to the ``stable/1.6.X`` - branch, and to the ``stable/1.5.X`` branch. They will trigger the release of +* Security fixes will be applied to ``master``, to the ``stable/1.6.x`` + branch, and to the ``stable/1.5.x`` branch. They will trigger the release of ``1.6.1``, ``1.5.1``, etc. * Documentation fixes will be applied to master, and, if easily backported, to - the ``1.6.X`` branch. Bugfixes may also be backported. + the ``1.6.x`` branch. Bugfixes may also be backported. .. _release-process: @@ -193,9 +189,9 @@ Phase two will culminate with an alpha release. At this point, the Phase three: bugfixes ~~~~~~~~~~~~~~~~~~~~~ -The last third of a release is spent fixing bugs -- no new features will be -accepted during this time. We'll try to release a beta release after one month -and a release candidate after two months. +The last third of a release cycle is spent fixing bugs -- no new features will +be accepted during this time. We'll try to release a beta release after one +month and a release candidate after two months. The release candidate marks the string freeze, and it happens at least two weeks before the final release. After this point, new translatable strings -- 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') 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 6845dc7e8f6e4f5422e9381f5e045e1a521a76b4 Mon Sep 17 00:00:00 2001 From: "Panagiotis H.M. Issaris" Date: Thu, 23 May 2013 10:19:10 +0200 Subject: Fixed comments for translators example in the i18n documentation The documentation regarding adding a comment for translators was confusing as the example contained mismatching tags. --- docs/topics/i18n/translation.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 433d40b0bb..ce6697908f 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -698,7 +698,7 @@ or with the ``{#`` ... ``#}`` :ref:`one-line comment constructs {% trans "Go" %} {# Translators: This is a text of the base template #} -- cgit v1.3 From 8c2fd050f80f528cc1609c1a7f16901194834831 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Tue, 21 May 2013 18:32:39 -0300 Subject: Made fix for #9321 less buggy and more effective. Don't try to be smart about building a good-looking help string because it evaluates translations too early, simply use the same old strategy as before. Thanks Donald Stufft for the report. Also, actually fix the case reported by the OP by special-casing CheckboxSelectMultiple. Added tests. Refs #9321. --- django/forms/models.py | 7 ++++--- docs/releases/1.6.txt | 8 ++++---- tests/admin_widgets/tests.py | 12 ++++++++++++ tests/model_forms/models.py | 4 ++++ tests/model_forms/tests.py | 35 ++++++++++++++++++++++++++++++++++- 5 files changed, 58 insertions(+), 8 deletions(-) (limited to 'docs') diff --git a/django/forms/models.py b/django/forms/models.py index 68c1341cf7..9d5aa5d4d8 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -13,7 +13,7 @@ from django.forms.forms import BaseForm, get_declared_fields from django.forms.formsets import BaseFormSet, formset_factory from django.forms.util import ErrorList from django.forms.widgets import (SelectMultiple, HiddenInput, - MultipleHiddenInput, media_property) + MultipleHiddenInput, media_property, CheckboxSelectMultiple) from django.utils.encoding import smart_text, force_text from django.utils.datastructures import SortedDict from django.utils import six @@ -1104,9 +1104,10 @@ class ModelMultipleChoiceField(ModelChoiceField): super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices, required, widget, label, initial, help_text, *args, **kwargs) - if isinstance(self.widget, SelectMultiple): + # Remove this in Django 1.8 + if isinstance(self.widget, SelectMultiple) and not isinstance(self.widget, CheckboxSelectMultiple): msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') - self.help_text = string_concat(self.help_text, ' ', msg) if self.help_text else msg + self.help_text = string_concat(self.help_text, ' ', msg) def clean(self, value): if self.required and not value: diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 3bc5a0996c..5ee454d4f8 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -522,10 +522,10 @@ facilities together with Django built-in form :doc:`fields ` and :doc:`widgets ` aren't affected but need to be aware of what's described in :ref:`m2m-help_text-deprecation` below. -This is because, as an temporary backward-compatible provision, the described -non-standard behavior has been preserved but moved to the model form field layer -and occurs only when the associated widget is -:class:`~django.forms.SelectMultiple` or a subclass. +This is because, as an ad-hoc temporary backward-compatibility provision, the +described non-standard behavior has been preserved but moved to the model form +field layer and occurs only when the associated widget is +:class:`~django.forms.SelectMultiple` or selected subclasses. QuerySet iteration ~~~~~~~~~~~~~~~~~~ diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 4e09922893..98f41c1490 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -13,6 +13,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models import CharField, DateField from django.test import TestCase as DjangoTestCase from django.test.utils import override_settings +from django.utils import six from django.utils import translation from django.utils.html import conditional_escape from django.utils.unittest import TestCase @@ -139,6 +140,17 @@ class AdminFormfieldForDBFieldTests(TestCase): def testInheritance(self): self.assertFormfield(models.Album, 'backside_art', widgets.AdminFileWidget) + def test_m2m_widgets(self): + """m2m fields help text as it applies to admin app (#9321).""" + class AdvisorAdmin(admin.ModelAdmin): + filter_vertical=['companies'] + + self.assertFormfield(models.Advisor, 'companies', widgets.FilteredSelectMultiple, + filter_vertical=['companies']) + ma = AdvisorAdmin(models.Advisor, admin.site) + f = ma.formfield_for_dbfield(models.Advisor._meta.get_field('companies'), request=None) + self.assertEqual(six.text_type(f.help_text), ' Hold down "Control", or "Command" on a Mac, to select more than one.') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminFormfieldForDBFieldWithRequestTests(DjangoTestCase): diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index a79d9b8c5b..9c5e097106 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -255,3 +255,7 @@ class Colour(models.Model): class ColourfulItem(models.Model): name = models.CharField(max_length=50) colours = models.ManyToManyField(Colour) + +class ArticleStatusNote(models.Model): + name = models.CharField(max_length=20) + status = models.ManyToManyField(ArticleStatus) diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index db8b6cf0e7..5219804e0c 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, - test_images) + ArticleStatusNote, test_images) if test_images: from .models import ImageFile, OptionalImageFile @@ -234,6 +234,20 @@ class ColourfulItemForm(forms.ModelForm): model = ColourfulItem fields = '__all__' +# model forms for testing work on #9321: + +class StatusNoteForm(forms.ModelForm): + class Meta: + model = ArticleStatusNote + fields = '__all__' + + +class StatusNoteCBM2mForm(forms.ModelForm): + class Meta: + model = ArticleStatusNote + fields = '__all__' + widgets = {'status': forms.CheckboxSelectMultiple} + class ModelFormBaseTest(TestCase): def test_base_form(self): @@ -1677,3 +1691,22 @@ class OldFormForXTests(TestCase): Hold down "Control", or "Command" on a Mac, to select more than one.

            """ % {'blue_pk': colour.pk}) + + +class M2mHelpTextTest(TestCase): + """Tests for ticket #9321.""" + def test_multiple_widgets(self): + """Help text of different widgets for ManyToManyFields model fields""" + dreaded_help_text = ' Hold down "Control", or "Command" on a Mac, to select more than one.' + + # Default widget (SelectMultiple): + std_form = StatusNoteForm() + self.assertInHTML(dreaded_help_text, std_form.as_p()) + + # Overridden widget (CheckboxSelectMultiple, a subclass of + # SelectMultiple but with a UI that doesn't involve Control/Command + # keystrokes to extend selection): + form = StatusNoteCBM2mForm() + html = form.as_p() + self.assertInHTML('
              ', html) + self.assertInHTML(dreaded_help_text, html, count=0) -- 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') 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 8aca2504df9d7d3c1244d1632f6cad45afa60115 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 22 May 2013 21:12:55 -0400 Subject: Fixed #20080 - Recommended use of PYTHONHASHSEED Thanks jacob for the suggestion and ryankask for the patch. --- docs/howto/deployment/checklist.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'docs') diff --git a/docs/howto/deployment/checklist.txt b/docs/howto/deployment/checklist.txt index b72be75497..4498c78e3f 100644 --- a/docs/howto/deployment/checklist.txt +++ b/docs/howto/deployment/checklist.txt @@ -212,3 +212,18 @@ Miscellaneous -------------------------------- This setting is required if you're using the :ttag:`ssi` template tag. + +Python Options +============== + +If you're using Python 2.6.8+, it's strongly recommended that you invoke the +Python process running your Django application using the `-R`_ option or with +the :envvar:`PYTHONHASHSEED` environment variable set to ``random``. + +These options help protect your site from denial-of-service (DoS) +attacks triggered by carefully crafted inputs. Such an attack can +drastically increase CPU usage by causing worst-case performance when +creating ``dict`` instances. See `oCERT advisory #2011-003 +`_ for more information. + +.. _-r: http://docs.python.org/2.7/using/cmdline.html#cmdoption-R -- cgit v1.3 From 9e65bd33db5d211e114f6193ec2f70ccd4947e72 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 23 May 2013 08:36:53 -0400 Subject: Fixed #20482 - Added a file reference in tutorial 3. Thanks diek@ for the suggestion. --- docs/intro/tutorial03.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 0bbfcdd02f..120369172e 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -336,7 +336,7 @@ Put the following code in that template:

              No polls are available.

              {% endif %} -Now let's use that html template in our index view:: +Now let's update our ``index`` view in ``polls/views.py`` to use the template:: from django.http import HttpResponse from django.template import Context, loader -- cgit v1.3 From a46a399534c26abd3f4984149b820f34869d6b74 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 23 May 2013 08:52:55 -0400 Subject: Fixed #20417 - Noted that a server restart is required to load new templatetag modules. Thanks Katya for the suggestion. --- docs/howto/custom-template-tags.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 7611d44920..35f78718e4 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -20,7 +20,8 @@ create a new app to hold them. The app should contain a ``templatetags`` directory, at the same level as ``models.py``, ``views.py``, etc. If this doesn't already exist, create it - don't forget the ``__init__.py`` file to ensure the directory is treated as a -Python package. +Python package. After adding this module, you will need to restart your server +before you can use the tags or filters in templates. Your custom tags and filters will live in a module inside the ``templatetags`` directory. The name of the module file is the name you'll use to load the tags -- cgit v1.3 From 499a745ae1b53614035b9993b148f32d4ce3f138 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Tue, 21 May 2013 11:11:05 +0200 Subject: Fixed #20474 -- Proxied and deprecated django.db.backend --- django/db/__init__.py | 24 +++++++++++++++++++++++- docs/internals/deprecation.txt | 1 + tests/backends/tests.py | 9 ++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/django/db/__init__.py b/django/db/__init__.py index 08c901ab7b..077e1b2d8b 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -8,6 +8,7 @@ from django.db.utils import (DEFAULT_DB_ALIAS, ProgrammingError, NotSupportedError, DatabaseError, InterfaceError, Error, load_backend, ConnectionHandler, ConnectionRouter) +from django.utils.functional import cached_property __all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError', 'IntegrityError', 'DEFAULT_DB_ALIAS') @@ -45,7 +46,28 @@ class DefaultConnectionProxy(object): return delattr(connections[DEFAULT_DB_ALIAS], name) connection = DefaultConnectionProxy() -backend = load_backend(connection.settings_dict['ENGINE']) + +class DefaultBackendProxy(object): + """ + Temporary proxy class used during deprecation period of the `backend` module + variable. + """ + @cached_property + def _backend(self): + warnings.warn("Accessing django.db.backend is deprecated.", + PendingDeprecationWarning, stacklevel=2) + return load_backend(connections[DEFAULT_DB_ALIAS].settings_dict['ENGINE']) + + def __getattr__(self, item): + return getattr(self._backend, item) + + def __setattr__(self, name, value): + return setattr(self._backend, name, value) + + def __delattr__(self, name): + return delattr(self._backend, name) + +backend = DefaultBackendProxy() def close_connection(**kwargs): warnings.warn( diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 5fa7ea16ac..c4bbc3b7a4 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -373,6 +373,7 @@ these changes. * The following private APIs will be removed: + - ``django.db.backend`` - ``django.db.close_connection()`` - ``django.db.backends.creation.BaseDatabaseCreation.set_autocommit()`` - ``django.db.transaction.is_managed()`` diff --git a/tests/backends/tests.py b/tests/backends/tests.py index 79e3dd444e..08cae0f7c3 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -8,7 +8,7 @@ import threading from django.conf import settings from django.core.management.color import no_style -from django.db import (backend, connection, connections, DEFAULT_DB_ALIAS, +from django.db import (connection, connections, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError, transaction) from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2 import version as pg_version @@ -50,7 +50,8 @@ class OracleChecks(unittest.TestCase): def test_dbms_session(self): # If the backend is Oracle, test that we can call a standard # stored procedure through our cursor wrapper. - convert_unicode = backend.convert_unicode + from django.db.backends.oracle.base import convert_unicode + cursor = connection.cursor() cursor.callproc(convert_unicode('DBMS_SESSION.SET_IDENTIFIER'), [convert_unicode('_django_testing!')]) @@ -60,8 +61,10 @@ class OracleChecks(unittest.TestCase): def test_cursor_var(self): # If the backend is Oracle, test that we can pass cursor variables # as query parameters. + from django.db.backends.oracle.base import Database + cursor = connection.cursor() - var = cursor.var(backend.Database.STRING) + var = cursor.var(Database.STRING) cursor.execute("BEGIN %s := 'X'; END; ", [var]) self.assertEqual(var.getvalue(), 'X') -- cgit v1.3 From ec5dc0010c30f9ca7e9294b97e909f5b47ac1683 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Thu, 23 May 2013 15:14:31 +0100 Subject: Fixing some FIXMEs in howto-release-django. Refs #20082 --- docs/internals/howto-release-django.txt | 49 +++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 8 deletions(-) (limited to 'docs') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index fd985ddafc..5bda2e8add 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -183,13 +183,46 @@ OK, this is the fun part, where we actually push out a release! $ md5sum dist/Django-* $ sha1sum dist/Django-* - *FIXME: perhaps we should switch to sha256?* - #. Create a "checksums" file containing the hashes and release information. - You can start with `a previous checksums file`__ and replace the - dates, keys, links, and checksums. *FIXME: make a template file.* + Start with this template and insert the correct version, date, release URL + and checksums:: + + This file contains MD5 and SHA1 checksums for the source-code tarball + of Django <>, released <>. + + To use this file, you will need a working install of PGP or other + compatible public-key encryption software. You will also need to have + the Django release manager's public key in your keyring; this key has + the ID ``0x3684C0C08C8B2AE1`` and can be imported from the MIT + keyserver. For example, if using the open-source GNU Privacy Guard + implementation of PGP:: + + gpg --keyserver pgp.mit.edu --recv-key 0x3684C0C08C8B2AE1 + + Once the key is imported, verify this file:: + + gpg --verify <> + + Once you have verified this file, you can use normal MD5 and SHA1 + checksumming applications to generate the checksums of the Django + package and compare them to the checksums listed below. + + + Release package: + ================ + + Django <>: https://www.djangoproject.com/m/releases/<> + + + MD5 checksum: + ============= + + MD5(<>)= <> + + SHA1 checksum: + ============== - __ https://www.djangoproject.com/m/pgp/Django-1.5b1.checksum.txt + SHA1(<>)= <> #. Sign the checksum file (``gpg --clearsign Django-.checksum.txt``). This generates a signed document, @@ -268,8 +301,7 @@ Now you're ready to actually put the release out there. To do this: of the docs by flipping the ``is_default`` flag to ``True`` on the appropriate ``DocumentRelease`` object in the ``docs.djangoproject.com`` database (this will automatically flip it to ``False`` for all - others). *FIXME: I had to do this via fab managepy:shell,docs but we should - probably make it possible to do via the admin.* + others); you can do this using the site's admin. #. Post the release announcement to the django-announce, django-developers and django-users mailing lists. This should @@ -289,7 +321,8 @@ You're almost done! All that's left to do now is: ``stable/1.?.x`` git branch), you'll want to create a new ``DocumentRelease`` object in the ``docs.djangoproject.com`` database for the new version's docs, and update the ``docs/fixtures/doc_releases.json`` - JSON fixture. *FIXME: what is the purpose of maintaining this fixture?* + JSON fixture, so people without access to the production DB can still + run an up-to-date copy of the docs site. #. Add the release in `Trac's versions list`_ if necessary. Not all versions are declared; take example on previous releases. -- cgit v1.3 From cf007f0cbfe936ff699d761b243343201ca5c195 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 23 May 2013 07:22:52 -0700 Subject: Update my employer. --- docs/internals/committers.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 131f9c0f26..ffe7f578f9 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -243,7 +243,7 @@ Karen Tracey .. _James Tauber: http://jtauber.com/ `Alex Gaynor`_ - Alex is a software engineer working at Rdio_. He found Django in 2007 and + Alex is a software engineer working at Rackspace_. He found Django in 2007 and has been addicted ever since he found out you don't need to write out your forms by hand. He has a small obsession with compilers. He's contributed to the ORM, forms, admin, and other components of Django. @@ -251,7 +251,7 @@ Karen Tracey Alex lives in San Francisco, CA, USA. .. _Alex Gaynor: http://alexgaynor.net -.. _Rdio: http://rdio.com +.. _Rackspace: http://www.rackspace.com `Simon Meers`_ Simon discovered Django 0.96 during his Computer Science PhD research and -- cgit v1.3 From 5a62236b169e74f627492a90b091e8c9339a0181 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 23 May 2013 10:48:36 -0400 Subject: Added back a link to docs/internals/committers.txt --- docs/internals/committers.txt | 1 + 1 file changed, 1 insertion(+) (limited to 'docs') diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index ffe7f578f9..9400f1747c 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -53,6 +53,7 @@ Journal-World`_ of Lawrence, Kansas, USA. .. _revolution systems: http://revsys.com/ .. _wilson miner: http://wilsonminer.com/ .. _heroku: http://heroku.com/ +.. _Rdio: http://rdio.com Current developers ================== -- cgit v1.3 From dffdca1109a2111f104f2419d081c0f971537fec Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 23 May 2013 10:57:44 -0400 Subject: Fixed #20316 - Clarified transaction behavior of TestCase. Thanks uberj@ for the report and lolek09 for the patch. --- docs/topics/testing/overview.txt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index d543099ae6..3669bff8bf 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -925,14 +925,23 @@ to test the effects of commit and rollback: * A ``TestCase``, on the other hand, does not truncate tables after a test. Instead, it encloses the test code in a database transaction that is rolled - back at the end of the test. It also prevents the code under test from - issuing any commit or rollback operations on the database, to ensure that the - rollback at the end of the test restores the database to its initial state. + back at the end of the test. Both explicit commits like + ``transaction.commit()`` and implicit ones that may be caused by + ``Model.save()`` are replaced with a ``nop`` operation. This guarantees that + the rollback at the end of the test restores the database to its initial + state. When running on a database that does not support rollback (e.g. MySQL with the MyISAM storage engine), ``TestCase`` falls back to initializing the database by truncating tables and reloading initial data. +.. warning:: + + While ``commit`` and ``rollback`` operations still *appear* to work when + used in ``TestCase``, no actual commit or rollback will be performed by the + database. This can cause your tests to pass or fail unexpectedly. Always + use ``TransactionalTestCase`` when testing transactional behavior. + .. note:: .. versionchanged:: 1.5 -- 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') 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') 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') 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 f3ba6495e29d84f8d98a2acd390821ff0d23fd7a Mon Sep 17 00:00:00 2001 From: Brian Rosner Date: Fri, 24 May 2013 08:22:08 -0600 Subject: Updated my bio --- docs/internals/committers.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 9400f1747c..b56c8e469b 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -174,10 +174,10 @@ Joseph Kocherhans .. _charango: http://en.wikipedia.org/wiki/Charango `Brian Rosner`_ - Brian is currently the tech lead at Eldarion_ managing and developing + Brian is the Chief Architect at Eldarion_ managing and developing Django / Pinax_ based Web sites. He enjoys learning more about programming languages and system architectures and contributing to open source - projects. Brian is the host of the `Django Dose`_ podcasts. + projects. Brian helped immensely in getting Django's "newforms-admin" branch finished in time for Django 1.0; he's now a full committer, continuing to improve on @@ -187,7 +187,6 @@ Joseph Kocherhans .. _brian rosner: http://brosner.com/ .. _eldarion: http://eldarion.com/ -.. _django dose: http://djangodose.com/ .. _pinax: http://pinaxproject.com/ Justin Bronn -- 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') 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 6de81d65f443a01961c23139ca5d7653ef012d35 Mon Sep 17 00:00:00 2001 From: Vlastimil Zíma Date: Mon, 25 Mar 2013 15:45:24 +0100 Subject: Fixed #14825 -- LocaleMiddleware keeps language * LocaleMiddleware stores language into session if it is not present there. --- django/middleware/locale.py | 4 ++++ docs/releases/1.6.txt | 4 ++++ tests/i18n/tests.py | 32 ++++++++++++++++++++++++++++++++ tests/i18n/urls.py | 3 ++- 4 files changed, 42 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/django/middleware/locale.py b/django/middleware/locale.py index 25fbbaccdd..2e2204c859 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -53,6 +53,10 @@ class LocaleMiddleware(object): request.get_host(), language, request.get_full_path()) return HttpResponseRedirect(language_url) + # Store language back into session if it is not present + if hasattr(request, 'session'): + request.session.setdefault('django_language', language) + if not (self.is_language_prefix_patterns_used() and language_from_path): patch_vary_headers(response, ('Accept-Language',)) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 8d48381c06..35488b4110 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -266,6 +266,10 @@ Minor features ``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. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 777b9c4f75..019ed88cdb 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -1152,6 +1152,38 @@ class LocaleMiddlewareTests(TransRealMixin, TestCase): response = self.client.get('/en/streaming/') self.assertContains(response, "Yes/No") + @override_settings( + MIDDLEWARE_CLASSES=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + ), + ) + def test_session_language(self): + """ + Check that language is stored in session if missing. + """ + # Create an empty session + engine = import_module(settings.SESSION_ENGINE) + session = engine.SessionStore() + session.save() + self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key + + # Clear the session data before request + session.save() + response = self.client.get('/en/simple/') + self.assertEqual(self.client.session['django_language'], 'en') + + # Clear the session data before request + session.save() + response = self.client.get('/fr/simple/') + self.assertEqual(self.client.session['django_language'], 'fr') + + # Check that language is not changed in session + response = self.client.get('/en/simple/') + self.assertEqual(self.client.session['django_language'], 'fr') + + @override_settings( USE_I18N=True, LANGUAGES=( diff --git a/tests/i18n/urls.py b/tests/i18n/urls.py index c118265dab..c7d01897f5 100644 --- a/tests/i18n/urls.py +++ b/tests/i18n/urls.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals from django.conf.urls.i18n import i18n_patterns -from django.http import StreamingHttpResponse +from django.http import HttpResponse, StreamingHttpResponse from django.utils.translation import ugettext_lazy as _ urlpatterns = i18n_patterns('', + (r'^simple/$', lambda r: HttpResponse()), (r'^streaming/$', lambda r: StreamingHttpResponse([_("Yes"), "/", _("No")])), ) -- cgit v1.3 From f940e564e4623d531eb97a2cf1b116851003f9fd Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Fri, 24 May 2013 17:55:50 +0200 Subject: Fixed #20099 -- Eased subclassing of BrokenLinkEmailsMiddleware Thanks Ram Rachum for the report and the initial patch, and Simon Charette for the review. --- AUTHORS | 1 + django/middleware/common.py | 24 +++++++++++++++--------- docs/howto/error-reporting.txt | 5 +++++ tests/middleware/tests.py | 19 +++++++++++++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) (limited to 'docs') diff --git a/AUTHORS b/AUTHORS index b8047d92c9..b5f73f73c6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -472,6 +472,7 @@ answer newbie questions, and generally made Django that much better: Jyrki Pulliainen Thejaswi Puthraya Johann Queuniet + Ram Rachum Jan Rademaker Michael Radziej Laurent Rahuel diff --git a/django/middleware/common.py b/django/middleware/common.py index 250737970d..1131bd698d 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -142,15 +142,17 @@ class BrokenLinkEmailsMiddleware(object): domain = request.get_host() path = request.get_full_path() referer = force_text(request.META.get('HTTP_REFERER', ''), errors='replace') - is_internal = self.is_internal_request(domain, referer) - is_not_search_engine = '?' not in referer - is_ignorable = self.is_ignorable_404(path) - if referer and (is_internal or is_not_search_engine) and not is_ignorable: + + if not self.is_ignorable_request(request, path, domain, referer): ua = request.META.get('HTTP_USER_AGENT', '') ip = request.META.get('REMOTE_ADDR', '') mail_managers( - "Broken %slink on %s" % (('INTERNAL ' if is_internal else ''), domain), - "Referrer: %s\nRequested URL: %s\nUser agent: %s\nIP address: %s\n" % (referer, path, ua, ip), + "Broken %slink on %s" % ( + ('INTERNAL ' if self.is_internal_request(domain, referer) else ''), + domain + ), + "Referrer: %s\nRequested URL: %s\nUser agent: %s\n" + "IP address: %s\n" % (referer, path, ua, ip), fail_silently=True) return response @@ -159,10 +161,14 @@ class BrokenLinkEmailsMiddleware(object): Returns True if the referring URL is the same domain as the current request. """ # Different subdomains are treated as different domains. - return re.match("^https?://%s/" % re.escape(domain), referer) + return bool(re.match("^https?://%s/" % re.escape(domain), referer)) - def is_ignorable_404(self, uri): + def is_ignorable_request(self, request, uri, domain, referer): """ - Returns True if a 404 at the given URL *shouldn't* notify the site managers. + Returns True if the given request *shouldn't* notify the site managers. """ + # '?' in referer is identified as search engine source + if (not referer or + (not self.is_internal_request(domain, referer) and '?' in referer)): + return True return any(pattern.search(uri) for pattern in settings.IGNORABLE_404_URLS) diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 27f11f4936..987a503e95 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -98,6 +98,11 @@ crawlers often request:: (Note that these are regular expressions, so we put a backslash in front of periods to escape them.) +If you'd like to customize the behavior of +:class:`django.middleware.common.BrokenLinkEmailsMiddleware` further (for +example to ignore requests coming from web crawlers), you should subclass it +and override its methods. + .. seealso:: 404 errors are logged using the logging framework. By default, these log diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index 1ff8390f31..265eb97c36 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -326,6 +326,25 @@ class BrokenLinkEmailsMiddlewareTest(TestCase): BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) self.assertEqual(len(mail.outbox), 1) + def test_custom_request_checker(self): + class SubclassedMiddleware(BrokenLinkEmailsMiddleware): + ignored_user_agent_patterns = (re.compile(r'Spider.*'), + re.compile(r'Robot.*')) + def is_ignorable_request(self, request, uri, domain, referer): + '''Check user-agent in addition to normal checks.''' + if super(SubclassedMiddleware, self).is_ignorable_request(request, uri, domain, referer): + return True + user_agent = request.META['HTTP_USER_AGENT'] + return any(pattern.search(user_agent) for pattern in + self.ignored_user_agent_patterns) + + self.req.META['HTTP_REFERER'] = '/another/url/' + self.req.META['HTTP_USER_AGENT'] = 'Spider machine 3.4' + SubclassedMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 0) + self.req.META['HTTP_USER_AGENT'] = 'My user agent' + SubclassedMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 1) class ConditionalGetMiddlewareTest(TestCase): urls = 'middleware.cond_get_urls' -- 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') 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 0fa8d43e742ee8b480d938a47836b5a3720c2677 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Sun, 26 May 2013 23:47:50 -0300 Subject: Replaced `and...or...` constructs with PEP 308 conditional expressions. --- django/contrib/admin/filters.py | 2 +- django/contrib/admin/helpers.py | 4 ++-- django/contrib/admin/templatetags/log.py | 2 +- django/contrib/admindocs/views.py | 4 ++-- django/contrib/auth/forms.py | 2 +- django/contrib/gis/geos/geometry.py | 10 +++++----- django/contrib/gis/utils/layermapping.py | 2 +- django/contrib/sitemaps/__init__.py | 2 +- django/contrib/staticfiles/finders.py | 2 +- .../contrib/staticfiles/management/commands/collectstatic.py | 8 +++----- django/core/files/locks.py | 2 +- django/core/files/move.py | 6 +++--- django/core/management/commands/createcachetable.py | 4 ++-- django/core/management/commands/runserver.py | 2 +- django/db/backends/creation.py | 2 +- django/db/backends/util.py | 2 +- django/db/models/query.py | 2 +- django/db/models/sql/aggregates.py | 2 +- django/forms/forms.py | 2 +- django/middleware/common.py | 2 +- django/middleware/locale.py | 2 +- django/utils/log.py | 4 ++-- django/views/debug.py | 2 +- docs/_ext/djangodocs.py | 2 +- tests/forms_tests/tests/test_forms.py | 2 +- 25 files changed, 37 insertions(+), 39 deletions(-) (limited to 'docs') diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index ff66d3e3f3..4131494515 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -216,7 +216,7 @@ class RelatedFieldListFilter(FieldListFilter): } FieldListFilter.register(lambda f: ( - hasattr(f, 'rel') and bool(f.rel) or + bool(f.rel) if hasattr(f, 'rel') else isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter) diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index adc2302587..320d3267a7 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -131,7 +131,7 @@ class AdminField(object): classes.append('required') if not self.is_first: classes.append('inline') - attrs = classes and {'class': ' '.join(classes)} or {} + attrs = {'class': ' '.join(classes)} if classes else {} return self.field.label_tag(contents=mark_safe(contents), attrs=attrs) def errors(self): @@ -144,7 +144,7 @@ class AdminReadonlyField(object): # {{ field.name }} must be a useful class name to identify the field. # For convenience, store other field-related data here too. if callable(field): - class_name = field.__name__ != '' and field.__name__ or '' + class_name = field.__name__ if field.__name__ != '' else '' else: class_name = field self.field = { diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py index f70ee2731d..1b9e6aa7ef 100644 --- a/django/contrib/admin/templatetags/log.py +++ b/django/contrib/admin/templatetags/log.py @@ -53,4 +53,4 @@ def get_admin_log(parser, token): if tokens[4] != 'for_user': raise template.TemplateSyntaxError( "Fourth argument to 'get_admin_log' must be 'for_user'") - return AdminLogNode(limit=tokens[1], varname=tokens[3], user=(len(tokens) > 5 and tokens[5] or None)) + return AdminLogNode(limit=tokens[1], varname=tokens[3], user=(tokens[5] if len(tokens) > 5 else None)) diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 4cadc8b657..c03883def7 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -39,7 +39,7 @@ def bookmarklets(request): admin_root = urlresolvers.reverse('admin:index') return render_to_response('admin_doc/bookmarklets.html', { 'root_path': admin_root, - 'admin_url': "%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root), + 'admin_url': "%s://%s%s" % ('https' if request.is_secure() else 'http', request.get_host(), admin_root), }, context_instance=RequestContext(request)) @staff_member_required @@ -287,7 +287,7 @@ def template_detail(request, template): templates.append({ 'file': template_file, 'exists': os.path.exists(template_file), - 'contents': lambda: os.path.exists(template_file) and open(template_file).read() or '', + 'contents': lambda: open(template_file).read() if os.path.exists(template_file) else '', 'site_id': settings_mod.SITE_ID, 'site': site_obj, 'order': list(settings_mod.TEMPLATE_DIRS).index(dir), diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 0e08d8ef31..edf2727b07 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -237,7 +237,7 @@ class PasswordResetForm(forms.Form): 'uid': int_to_base36(user.pk), 'user': user, 'token': token_generator.make_token(user), - 'protocol': use_https and 'https' or 'http', + 'protocol': 'https' if use_https else 'http', } subject = loader.render_to_string(subject_template_name, c) # Email subject *must not* contain newlines diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 01719c21d4..b088ec2dc4 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -384,7 +384,7 @@ class GEOSGeometry(GEOSBase, ListMixin): @property def wkt(self): "Returns the WKT (Well-Known Text) representation of this Geometry." - return wkt_w(self.hasz and 3 or 2).write(self).decode() + return wkt_w(3 if self.hasz else 2).write(self).decode() @property def hex(self): @@ -395,7 +395,7 @@ class GEOSGeometry(GEOSBase, ListMixin): """ # A possible faster, all-python, implementation: # str(self.wkb).encode('hex') - return wkb_w(self.hasz and 3 or 2).write_hex(self) + return wkb_w(3 if self.hasz else 2).write_hex(self) @property def hexewkb(self): @@ -407,7 +407,7 @@ class GEOSGeometry(GEOSBase, ListMixin): if self.hasz and not GEOS_PREPARE: # See: http://trac.osgeo.org/geos/ticket/216 raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.') - return ewkb_w(self.hasz and 3 or 2).write_hex(self) + return ewkb_w(3 if self.hasz else 2).write_hex(self) @property def json(self): @@ -427,7 +427,7 @@ class GEOSGeometry(GEOSBase, ListMixin): as a Python buffer. SRID and Z values are not included, use the `ewkb` property instead. """ - return wkb_w(self.hasz and 3 or 2).write(self) + return wkb_w(3 if self.hasz else 2).write(self) @property def ewkb(self): @@ -439,7 +439,7 @@ class GEOSGeometry(GEOSBase, ListMixin): if self.hasz and not GEOS_PREPARE: # See: http://trac.osgeo.org/geos/ticket/216 raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.') - return ewkb_w(self.hasz and 3 or 2).write(self) + return ewkb_w(3 if self.hasz else 2).write(self) @property def kml(self): diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index cb56b8bce2..a502aa280d 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -546,7 +546,7 @@ class LayerMapping(object): # Attempting to save. m.save(using=self.using) num_saved += 1 - if verbose: stream.write('%s: %s\n' % (is_update and 'Updated' or 'Saved', m)) + if verbose: stream.write('%s: %s\n' % ('Updated' if is_update else 'Saved', m)) except SystemExit: raise except Exception as msg: diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index 7d03ef19f5..1acae8296c 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -94,7 +94,7 @@ class Sitemap(object): 'location': loc, 'lastmod': self.__get('lastmod', item, None), 'changefreq': self.__get('changefreq', item, None), - 'priority': str(priority is not None and priority or ''), + 'priority': str(priority if priority is not None else ''), } urls.append(url_info) return urls diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py index 8631570e79..7d266d95a0 100644 --- a/django/contrib/staticfiles/finders.py +++ b/django/contrib/staticfiles/finders.py @@ -245,7 +245,7 @@ def find(path, all=False): if matches: return matches # No match. - return all and [] or None + return [] if all else None def get_finders(): diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index c620993f33..7c3de80e93 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -175,11 +175,9 @@ Type 'yes' to continue, or 'no' to cancel: """ summary = template % { 'modified_count': modified_count, 'identifier': 'static file' + ('' if modified_count == 1 else 's'), - 'action': self.symlink and 'symlinked' or 'copied', - 'destination': (destination_path and " to '%s'" - % destination_path or ''), - 'unmodified': (collected['unmodified'] and ', %s unmodified' - % unmodified_count or ''), + 'action': 'symlinked' if self.symlink else 'copied', + 'destination': (" to '%s'" % destination_path if destination_path else ''), + 'unmodified': (', %s unmodified' % unmodified_count if collected['unmodified'] else ''), 'post_processed': (collected['post_processed'] and ', %s post-processed' % post_processed_count or ''), diff --git a/django/core/files/locks.py b/django/core/files/locks.py index d1384329dc..6f0e4b9508 100644 --- a/django/core/files/locks.py +++ b/django/core/files/locks.py @@ -41,7 +41,7 @@ except (ImportError, AttributeError): def fd(f): """Get a filedescriptor from something which could be a file or an fd.""" - return hasattr(f, 'fileno') and f.fileno() or f + return f.fileno() if hasattr(f, 'fileno') else f if system_type == 'nt': def lock(file, flags): diff --git a/django/core/files/move.py b/django/core/files/move.py index 3af02634fe..4519dedf97 100644 --- a/django/core/files/move.py +++ b/django/core/files/move.py @@ -62,7 +62,7 @@ def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_ove with open(old_file_name, 'rb') as old_file: # now open the new file, not forgetting allow_overwrite fd = os.open(new_file_name, os.O_WRONLY | os.O_CREAT | getattr(os, 'O_BINARY', 0) | - (not allow_overwrite and os.O_EXCL or 0)) + (os.O_EXCL if not allow_overwrite else 0)) try: locks.lock(fd, locks.LOCK_EX) current_chunk = None @@ -77,8 +77,8 @@ def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_ove try: os.remove(old_file_name) except OSError as e: - # Certain operating systems (Cygwin and Windows) - # fail when deleting opened files, ignore it. (For the + # Certain operating systems (Cygwin and Windows) + # fail when deleting opened files, ignore it. (For the # systems where this happens, temporary files will be auto-deleted # on close anyway.) if getattr(e, 'winerror', 0) != 32 and getattr(e, 'errno', 0) != 13: diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py index 4d1bc0403e..d7ce3e93fd 100644 --- a/django/core/management/commands/createcachetable.py +++ b/django/core/management/commands/createcachetable.py @@ -38,7 +38,7 @@ class Command(LabelCommand): qn = connection.ops.quote_name for f in fields: field_output = [qn(f.name), f.db_type(connection=connection)] - field_output.append("%sNULL" % (not f.null and "NOT " or "")) + field_output.append("%sNULL" % ("NOT " if not f.null else "")) if f.primary_key: field_output.append("PRIMARY KEY") elif f.unique: @@ -51,7 +51,7 @@ class Command(LabelCommand): table_output.append(" ".join(field_output)) full_statement = ["CREATE TABLE %s (" % qn(tablename)] for i, line in enumerate(table_output): - full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or '')) + full_statement.append(' %s%s' % (line, ',' if i < len(table_output)-1 else '')) full_statement.append(');') with transaction.commit_on_success_unless_managed(): curs = connection.cursor() diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index 4fe03216d5..c4a0b78cd4 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -99,7 +99,7 @@ class Command(BaseCommand): "started_at": datetime.now().strftime('%B %d, %Y - %X'), "version": self.get_version(), "settings": settings.SETTINGS_MODULE, - "addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr, + "addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr, "port": self.port, "quit_command": quit_command, }) diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 38c284d6d3..c9e5c83ade 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -97,7 +97,7 @@ class BaseDatabaseCreation(object): style.SQL_TABLE(qn(opts.db_table)) + ' ('] for i, line in enumerate(table_output): # Combine and add commas. full_statement.append( - ' %s%s' % (line, i < len(table_output) - 1 and ',' or '')) + ' %s%s' % (line, ',' if i < len(table_output) - 1 else '')) full_statement.append(')') if opts.db_tablespace: tablespace_sql = self.connection.ops.tablespace_sql( diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 084f4c200b..aa2601277a 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -83,7 +83,7 @@ class CursorDebugWrapper(CursorWrapper): ############################################### def typecast_date(s): - return s and datetime.date(*map(int, s.split('-'))) or None # returns None if s is null + return datetime.date(*map(int, s.split('-'))) if s else None # returns None if s is null def typecast_time(s): # does NOT store time zone information if not s: return None diff --git a/django/db/models/query.py b/django/db/models/query.py index c54380b96f..71d432f7d5 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -125,7 +125,7 @@ class QuerySet(object): else: stop = None qs.query.set_limits(start, stop) - return k.step and list(qs)[::k.step] or qs + return list(qs)[::k.step] if k.step else qs qs = self._clone() qs.query.set_limits(k, k + 1) diff --git a/django/db/models/sql/aggregates.py b/django/db/models/sql/aggregates.py index 23b79923d1..2bd2b2f76f 100644 --- a/django/db/models/sql/aggregates.py +++ b/django/db/models/sql/aggregates.py @@ -99,7 +99,7 @@ class Count(Aggregate): sql_template = '%(function)s(%(distinct)s%(field)s)' def __init__(self, col, distinct=False, **extra): - super(Count, self).__init__(col, distinct=distinct and 'DISTINCT ' or '', **extra) + super(Count, self).__init__(col, distinct='DISTINCT ' if distinct else '', **extra) class Max(Aggregate): sql_function = 'MAX' diff --git a/django/forms/forms.py b/django/forms/forms.py index 2c173a45dc..0c598ac775 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -134,7 +134,7 @@ class BaseForm(object): Subclasses may wish to override. """ - return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name + return '%s-%s' % (self.prefix, field_name) if self.prefix else field_name def add_initial_prefix(self, field_name): """ diff --git a/django/middleware/common.py b/django/middleware/common.py index 1131bd698d..2c76c47756 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -85,7 +85,7 @@ class CommonMiddleware(object): return if new_url[0]: newurl = "%s://%s%s" % ( - request.is_secure() and 'https' or 'http', + 'https' if request.is_secure() else 'http', new_url[0], urlquote(new_url[1])) else: newurl = urlquote(new_url[1]) diff --git a/django/middleware/locale.py b/django/middleware/locale.py index 2e2204c859..4e0a4753ce 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -49,7 +49,7 @@ class LocaleMiddleware(object): if path_valid: language_url = "%s://%s/%s%s" % ( - request.is_secure() and 'https' or 'http', + 'https' if request.is_secure() else 'http', request.get_host(), language, request.get_full_path()) return HttpResponseRedirect(language_url) diff --git a/django/utils/log.py b/django/utils/log.py index 2abf7701cb..6734a7261e 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -92,8 +92,8 @@ class AdminEmailHandler(logging.Handler): request = record.request subject = '%s (%s IP): %s' % ( record.levelname, - (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS - and 'internal' or 'EXTERNAL'), + ('internal' if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS + else 'EXTERNAL'), record.getMessage() ) filter = get_exception_reporter_filter(request) diff --git a/django/views/debug.py b/django/views/debug.py index 209cdce8c9..4ecd936771 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -405,7 +405,7 @@ class ExceptionReporter(object): if pre_context_lineno is not None: frames.append({ 'tb': tb, - 'type': module_name.startswith('django.') and 'django' or 'user', + 'type': 'django' if module_name.startswith('django.') else 'user', 'filename': filename, 'function': function, 'lineno': lineno + 1, diff --git a/docs/_ext/djangodocs.py b/docs/_ext/djangodocs.py index 3ae770da20..833232a0c3 100644 --- a/docs/_ext/djangodocs.py +++ b/docs/_ext/djangodocs.py @@ -137,7 +137,7 @@ class DjangoHTMLTranslator(SmartyPantsHTMLTranslator): ) title = "%s%s" % ( self.version_text[node['type']] % node['version'], - len(node) and ":" or "." + ":" if len(node) else "." ) self.body.append('%s ' % title) diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index f7eb46522f..1a3fb44a66 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -1398,7 +1398,7 @@ class FormsTestCase(TestCase): birthday = DateField() def add_prefix(self, field_name): - return self.prefix and '%s-prefix-%s' % (self.prefix, field_name) or field_name + return '%s-prefix-%s' % (self.prefix, field_name) if self.prefix else field_name p = Person(prefix='foo') self.assertHTMLEqual(p.as_ul(), """
            • -- cgit v1.3 From f077f891986b3220f2b1223f491b0077595c0313 Mon Sep 17 00:00:00 2001 From: yishaibeeri Date: Mon, 27 May 2013 11:14:38 +0300 Subject: Patch for ticket #20506 - documentation type --- docs/topics/db/transactions.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 3193fe2c9a..e9a626f56b 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -564,7 +564,7 @@ API changes Transaction middleware ~~~~~~~~~~~~~~~~~~~~~~ -In Django 1.6, ``TransactionMiddleware`` is deprecated and replaced +In Django 1.6, ``TransactionMiddleware`` is deprecated and replaced by :setting:`ATOMIC_REQUESTS `. While the general behavior is the same, there are two differences. -- cgit v1.3 From 2a22540057d029507dc56c8203099887b35ed12d Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 27 May 2013 11:14:16 +0200 Subject: Fixed #20508 -- ReST typo. --- docs/howto/deployment/checklist.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/howto/deployment/checklist.txt b/docs/howto/deployment/checklist.txt index 4498c78e3f..1a235673e8 100644 --- a/docs/howto/deployment/checklist.txt +++ b/docs/howto/deployment/checklist.txt @@ -160,9 +160,10 @@ only useful in development. In addition, you can tune the following settings. :setting:`CONN_MAX_AGE` ----------------------- -Enabling `persistent database connections `_ -can result in a nice speed-up when connecting to the database accounts for a -significant part of the request processing time. +Enabling :ref:`persistent database connections +` can result in a nice speed-up when +connecting to the database accounts for a significant part of the request +processing time. This helps a lot on virtualized hosts with limited network performance. -- cgit v1.3 From 749c2e4333893671565124b1676f5398ef9d6368 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 27 May 2013 11:15:40 +0200 Subject: Fixed URL for six inventory. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/conf.py b/docs/conf.py index a654e3c4d6..a01ddb60b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -109,7 +109,7 @@ pygments_style = 'trac' intersphinx_mapping = { 'python': ('http://docs.python.org/2.7', None), 'sphinx': ('http://sphinx.pocoo.org/', None), - 'six': ('http://packages.python.org/six/', None), + 'six': ('http://pythonhosted.org/six/', None), 'simplejson': ('http://simplejson.readthedocs.org/en/latest/', None), } -- 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') 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 6f8627dd7fd482007694dd1e4a99f9e207e6d8c2 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 24 May 2013 13:30:37 -0400 Subject: Fixed #17582 - Added message to DoesNotExist exceptions. Thanks simon@ for the suggestion and JordanPowell for the initial patch. --- django/db/models/fields/related.py | 7 +++++-- docs/releases/1.6.txt | 3 +++ tests/foreign_object/tests.py | 5 +++++ tests/one_to_one/tests.py | 3 ++- 4 files changed, 15 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index fd0b24433f..fb38aa9c57 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -201,7 +201,9 @@ class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescri setattr(rel_obj, self.related.field.get_cache_name(), instance) setattr(instance, self.cache_name, rel_obj) if rel_obj is None: - raise self.related.model.DoesNotExist + raise self.related.model.DoesNotExist("%s has no %s." % ( + instance.__class__.__name__, + self.related.get_accessor_name())) else: return rel_obj @@ -304,7 +306,8 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec setattr(rel_obj, self.field.related.get_cache_name(), instance) setattr(instance, self.cache_name, rel_obj) if rel_obj is None and not self.field.null: - raise self.field.rel.to.DoesNotExist + raise self.field.rel.to.DoesNotExist( + "%s has no %s." % (self.field.model.__name__, self.field.name)) else: return rel_obj diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 825c5dc28d..73b0fb85eb 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -277,6 +277,9 @@ Minor features a ``SuspiciousOperation`` reaches the WSGI handler to return an ``HttpResponseBadRequest``. +* The :exc:`~django.core.exceptions.DoesNotExist` exception now includes a + message indicating the name of the attribute used for the lookup. + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index 55dd6a0f47..69636ee49b 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -316,6 +316,11 @@ class MultiColumnFKTests(TestCase): list(Article.objects.filter(active_translation__abstract=None)), [a1, a2]) + def test_foreign_key_raises_informative_does_not_exist(self): + referrer = ArticleTranslation() + with self.assertRaisesMessage(Article.DoesNotExist, 'ArticleTranslation has no article'): + referrer.article + class FormsTests(TestCase): # ForeignObjects should not have any form fields, currently the user needs # to manually deal with the foreignobject relation. diff --git a/tests/one_to_one/tests.py b/tests/one_to_one/tests.py index 6ee78520bd..a36764b788 100644 --- a/tests/one_to_one/tests.py +++ b/tests/one_to_one/tests.py @@ -22,7 +22,8 @@ class OneToOneTests(TestCase): # A Place can access its restaurant, if available. self.assertEqual(repr(self.p1.restaurant), '') # p2 doesn't have an associated restaurant. - self.assertRaises(Restaurant.DoesNotExist, getattr, self.p2, 'restaurant') + with self.assertRaisesMessage(Restaurant.DoesNotExist, 'Place has no restaurant'): + self.p2.restaurant def test_setter(self): # Set the place using assignment notation. Because place is the primary -- 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') 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 d194714c0a707773bd470bffb3d67a60e40bb787 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 8 Feb 2013 18:45:26 -0500 Subject: Fixed #11603 - Added django.test.SimpleTestCase.assertFormsetError Thank-you Martin Green for the patch. --- AUTHORS | 1 + django/test/testcases.py | 77 +++++++++++++++ docs/releases/1.6.txt | 4 + docs/topics/testing/overview.txt | 21 ++++ tests/test_client/urls.py | 1 + tests/test_client/views.py | 46 ++++++++- tests/test_client_regress/tests.py | 191 +++++++++++++++++++++++++++++++++++++ 7 files changed, 340 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/AUTHORS b/AUTHORS index b5f73f73c6..9c0af6a03a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -253,6 +253,7 @@ answer newbie questions, and generally made Django that much better: pradeep.gowda@gmail.com Collin Grady Gabriel Grant + Martin Green Daniel Greenfeld Simon Greenhill Owen Griffiths diff --git a/django/test/testcases.py b/django/test/testcases.py index 311b50cfb7..463914c1e1 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -509,6 +509,83 @@ class SimpleTestCase(ut2.TestCase): self.fail(msg_prefix + "The form '%s' was not used to render the" " response" % form) + def assertFormsetError(self, response, formset, form_index, field, errors, + msg_prefix=''): + """ + Asserts that a formset used to render the response has a specific error. + + For field errors, specify the ``form_index`` and the ``field``. + For non-field errors, specify the ``form_index`` and the ``field`` as + None. + For non-form errors, specify ``form_index`` as None and the ``field`` + as None. + """ + # Add punctuation to msg_prefix + if msg_prefix: + msg_prefix += ": " + + # Put context(s) into a list to simplify processing. + contexts = to_list(response.context) + if not contexts: + self.fail(msg_prefix + 'Response did not use any contexts to ' + 'render the response') + + # Put error(s) into a list to simplify processing. + errors = to_list(errors) + + # Search all contexts for the error. + found_formset = False + for i, context in enumerate(contexts): + if formset not in context: + continue + found_formset = True + for err in errors: + if field is not None: + if field in context[formset].forms[form_index].errors: + field_errors = context[formset].forms[form_index].errors[field] + self.assertTrue(err in field_errors, + msg_prefix + "The field '%s' on formset '%s', " + "form %d in context %d does not contain the " + "error '%s' (actual errors: %s)" % + (field, formset, form_index, i, err, + repr(field_errors))) + elif field in context[formset].forms[form_index].fields: + self.fail(msg_prefix + "The field '%s' " + "on formset '%s', form %d in " + "context %d contains no errors" % + (field, formset, form_index, i)) + else: + self.fail(msg_prefix + "The formset '%s', form %d in " + "context %d does not contain the field '%s'" % + (formset, form_index, i, field)) + elif form_index is not None: + non_field_errors = context[formset].forms[form_index].non_field_errors() + self.assertFalse(len(non_field_errors) == 0, + msg_prefix + "The formset '%s', form %d in " + "context %d does not contain any non-field " + "errors." % (formset, form_index, i)) + self.assertTrue(err in non_field_errors, + msg_prefix + "The formset '%s', form %d " + "in context %d does not contain the " + "non-field error '%s' " + "(actual errors: %s)" % + (formset, form_index, i, err, + repr(non_field_errors))) + else: + non_form_errors = context[formset].non_form_errors() + self.assertFalse(len(non_form_errors) == 0, + msg_prefix + "The formset '%s' in " + "context %d does not contain any " + "non-form errors." % (formset, i)) + self.assertTrue(err in non_form_errors, + msg_prefix + "The formset '%s' in context " + "%d does not contain the " + "non-form error '%s' (actual errors: %s)" % + (formset, i, err, repr(non_form_errors))) + if not found_formset: + self.fail(msg_prefix + "The formset '%s' was not used to render " + "the response" % formset) + def assertTemplateUsed(self, response=None, template_name=None, msg_prefix=''): """ Asserts that the template with the provided name was used in rendering diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 4a8f7c6ecf..4f4b615835 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -283,6 +283,10 @@ Minor features * The :meth:`~django.db.models.query.QuerySet.get_or_create` method no longer requires at least one keyword argument. +* The :class:`~django.test.SimpleTestCase` class includes a new assertion + helper for testing formset errors: + :meth:`~django.test.SimpleTestCase.assertFormsetError`. + Backwards incompatible changes in 1.6 ===================================== diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 3669bff8bf..470051e78e 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -1532,6 +1532,27 @@ your test suite. ``errors`` is an error string, or a list of error strings, that are expected as a result of form validation. +.. method:: SimpleTestCase.assertFormsetError(response, formset, form_index, field, errors, msg_prefix='') + + .. versionadded:: 1.6 + + Asserts that the ``formset`` raises the provided list of errors when + rendered. + + ``formset`` is the name the ``Formset`` instance was given in the template + context. + + ``form_index`` is the number of the form within the ``Formset``. If + ``form_index`` has a value of ``None``, non-form errors (errors you can + access via ``formset.non_form_errors()``) will be checked. + + ``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:: SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) Asserts that a ``Response`` instance produced the given ``status_code`` and diff --git a/tests/test_client/urls.py b/tests/test_client/urls.py index 67c475eaff..bd395ca552 100644 --- a/tests/test_client/urls.py +++ b/tests/test_client/urls.py @@ -21,6 +21,7 @@ urlpatterns = patterns('', (r'^bad_view/$', views.bad_view), (r'^form_view/$', views.form_view), (r'^form_view_with_template/$', views.form_view_with_template), + (r'^formset_view/$', views.formset_view), (r'^login_protected_view/$', views.login_protected_view), (r'^login_protected_method_view/$', views.login_protected_method_view), (r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect), diff --git a/tests/test_client/views.py b/tests/test_client/views.py index f760466497..76296cb80d 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -7,7 +7,8 @@ from xml.dom.minidom import parseString from django.contrib.auth.decorators import login_required, permission_required from django.core import mail from django.forms import fields -from django.forms.forms import Form +from django.forms.forms import Form, ValidationError +from django.forms.formsets import formset_factory, BaseFormSet from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound from django.shortcuts import render_to_response from django.template import Context, Template @@ -95,6 +96,12 @@ class TestForm(Form): single = fields.ChoiceField(choices=TestChoices) multi = fields.MultipleChoiceField(choices=TestChoices) + def clean(self): + cleaned_data = self.cleaned_data + if cleaned_data.get("text") == "Raise non-field error": + raise ValidationError("Non-field error.") + return cleaned_data + def form_view(request): "A view that tests a simple form" if request.method == 'POST': @@ -130,6 +137,43 @@ def form_view_with_template(request): } ) +class BaseTestFormSet(BaseFormSet): + def clean(self): + """Checks that no two email addresses are the same.""" + if any(self.errors): + # Don't bother validating the formset unless each form is valid + return + + emails = [] + for i in range(0, self.total_form_count()): + form = self.forms[i] + email = form.cleaned_data['email'] + if email in emails: + raise ValidationError( + "Forms in a set must have distinct email addresses." + ) + emails.append(email) + +TestFormSet = formset_factory(TestForm, BaseTestFormSet) + +def formset_view(request): + "A view that tests a simple formset" + if request.method == 'POST': + formset = TestFormSet(request.POST) + if formset.is_valid(): + t = Template('Valid POST data.', name='Valid POST Template') + c = Context() + else: + t = Template('Invalid POST data. {{ my_formset.errors }}', + name='Invalid POST Template') + c = Context({'my_formset': formset}) + else: + formset = TestForm(request.GET) + t = Template('Viewing base formset. {{ my_formset }}.', + name='Formset GET Template') + c = Context({'my_formset': formset}) + return HttpResponse(t.render(c)) + def login_protected_view(request): "A simple view that is login protected." t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template') diff --git a/tests/test_client_regress/tests.py b/tests/test_client_regress/tests.py index 6a58ef6344..654f65235a 100644 --- a/tests/test_client_regress/tests.py +++ b/tests/test_client_regress/tests.py @@ -543,6 +543,197 @@ class AssertFormErrorTests(TestCase): except AssertionError as e: self.assertIn("abc: The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )", str(e)) +class AssertFormsetErrorTests(TestCase): + msg_prefixes = [("", {}), ("abc: ", {"msg_prefix": "abc"})] + def setUp(self): + """Makes response object for testing field and non-field errors""" + # For testing field and non-field errors + self.response_form_errors = self.getResponse({ + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-0-text': 'Raise non-field error', + 'form-0-email': 'not an email address', + 'form-0-value': 37, + 'form-0-single': 'b', + 'form-0-multi': ('b','c','e'), + 'form-1-text': 'Hello World', + 'form-1-email': 'email@domain.com', + 'form-1-value': 37, + 'form-1-single': 'b', + 'form-1-multi': ('b','c','e'), + }) + # For testing non-form errors + self.response_nonform_errors = self.getResponse({ + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-0-text': 'Hello World', + 'form-0-email': 'email@domain.com', + 'form-0-value': 37, + 'form-0-single': 'b', + 'form-0-multi': ('b','c','e'), + 'form-1-text': 'Hello World', + 'form-1-email': 'email@domain.com', + 'form-1-value': 37, + 'form-1-single': 'b', + 'form-1-multi': ('b','c','e'), + }) + + def getResponse(self, post_data): + response = self.client.post('/test_client/formset_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + return response + + def test_unknown_formset(self): + "An assertion is raised if the formset name is unknown" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'wrong_formset', + 0, + 'Some_field', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'wrong_formset' was not " + "used to render the response", + str(cm.exception)) + + def test_unknown_field(self): + "An assertion is raised if the field name is unknown" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'Some_field', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset', " + "form 0 in context 0 " + "does not contain the field 'Some_field'", + str(cm.exception)) + + def test_no_error_field(self): + "An assertion is raised if the field doesn't have any errors" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 1, + 'value', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The field 'value' " + "on formset 'my_formset', form 1 " + "in context 0 contains no errors", + str(cm.exception)) + + def test_unknown_error(self): + "An assertion is raised if the field doesn't contain the specified error" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'email', + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + "The field 'email' " + "on formset 'my_formset', form 0 in context 0 does not " + "contain the error 'Some error.' (actual errors: " + "[%(_)s'Enter a valid email address.'])"), + str(cm.exception)) + + def test_field_error(self): + "No assertion is raised if the field contains the provided error" + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'email', + ['Enter a valid email address.'], + **kwargs) + + def test_no_nonfield_error(self): + "An assertion is raised if the formsets non-field errors doesn't contain any errors." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 1, + None, + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset', form 1 in " + "context 0 does not contain any " + "non-field errors.", + str(cm.exception)) + + def test_unknown_nonfield_error(self): + "An assertion is raised if the formsets non-field errors doesn't contain the provided error." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + None, + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + + "The formset 'my_formset', form 0 in context 0 does not " + "contain the non-field error 'Some error.' (actual errors: " + "[%(_)s'Non-field error.'])"), str(cm.exception)) + + def test_nonfield_error(self): + "No assertion is raised if the formsets non-field errors contains the provided error." + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + None, + 'Non-field error.', + **kwargs) + + def test_no_nonform_error(self): + "An assertion is raised if the formsets non-form errors doesn't contain any errors." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + None, + None, + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset' in context 0 " + "does not contain any non-form errors.", + str(cm.exception)) + + def test_unknown_nonform_error(self): + "An assertion is raised if the formsets non-form errors doesn't contain the provided error." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_nonform_errors, + 'my_formset', + None, + None, + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + + "The formset 'my_formset' in context 0 does not contain the " + "non-form error 'Some error.' (actual errors: [%(_)s'Forms " + "in a set must have distinct email addresses.'])"), str(cm.exception)) + + def test_nonform_error(self): + "No assertion is raised if the formsets non-form errors contains the provided error." + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_nonform_errors, + 'my_formset', + None, + None, + 'Forms in a set must have distinct email ' + 'addresses.', + **kwargs) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class LoginTests(TestCase): fixtures = ['testdata'] -- cgit v1.3 From cd79f337233be3f2dfa22316314c9d4834093e31 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 27 May 2013 14:41:39 -0600 Subject: Fixed #20503 - Moved doctest utilities in with the rest of the deprecated test code. The ``DocTestRunner`` and ``OutputChecker`` were formerly in ``django.test.testcases``, now they are in ``django.test.simple``. This avoids triggering the ``django.test._doctest`` deprecation message with any import from ``django.test``. Since these utility classes are undocumented internal API, they can be moved without a separate deprecation process. Also removed the deprecation warnings specific to these classes, as they are now covered by the module-level warning in ``django.test.simple``. Thanks Anssi for the report. Refs #17365. --- django/test/simple.py | 70 +++++++++++++++++++++++++++++++++++-- django/test/testcases.py | 79 ++---------------------------------------- docs/internals/deprecation.txt | 6 ++-- docs/releases/1.6.txt | 18 ++++------ 4 files changed, 78 insertions(+), 95 deletions(-) (limited to 'docs') diff --git a/django/test/simple.py b/django/test/simple.py index 5117c6452f..f28b8a2830 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -3,14 +3,15 @@ This module is pending deprecation as of Django 1.6 and will be removed in version 1.8. """ - +import json +import re import unittest as real_unittest import warnings from django.db.models import get_app, get_apps from django.test import _doctest as doctest from django.test import runner -from django.test.testcases import OutputChecker, DocTestRunner +from django.test.utils import compare_xml, strip_quotes from django.utils import unittest from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule @@ -25,6 +26,71 @@ warnings.warn( # The module name for tests outside models.py TEST_MODULE = 'tests' + +normalize_long_ints = lambda s: re.sub(r'(? Date: Tue, 28 May 2013 08:41:13 -0400 Subject: Fixed #20513 - Expanded docs on QuerySet caching. Thanks seddonym. --- docs/topics/db/queries.txt | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 39ef2b33fd..bdbdd3fa2a 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -714,9 +714,9 @@ for you transparently. Caching and QuerySets --------------------- -Each :class:`~django.db.models.query.QuerySet` contains a cache, to minimize -database access. It's important to understand how it works, in order to write -the most efficient code. +Each :class:`~django.db.models.query.QuerySet` contains a cache to minimize +database access. Understanding how it works will allow you to write the most +efficient code. In a newly created :class:`~django.db.models.query.QuerySet`, the cache is empty. The first time a :class:`~django.db.models.query.QuerySet` is evaluated @@ -747,6 +747,43 @@ To avoid this problem, simply save the >>> print([p.headline for p in queryset]) # Evaluate the query set. >>> print([p.pub_date for p in queryset]) # Re-use the cache from the evaluation. +When querysets are not cached +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Querysets do not always cache their results. When evaluating only *part* of +the queryset, the cache is checked, but if it is not populated then the items +returned by the subsequent query are not cached. Specifically, this means that +:ref:`limiting the queryset ` using an array slice or an +index will not populate the cache. + +For example, repeatedly getting a certain index in a queryset object will query +the database each time:: + + >>> queryset = Entry.objects.all() + >>> print queryset[5] # Queries the database + >>> print queryset[5] # Queries the database again + +However, if the entire queryset has already been evaluated, the cache will be +checked instead:: + + >>> queryset = Entry.objects.all() + >>> [entry for entry in queryset] # Queries the database + >>> print queryset[5] # Uses cache + >>> print queryset[5] # Uses cache + +Here are some examples of other actions that will result in the entire queryset +being evaluated and therefore populate the cache:: + + >>> [entry for entry in queryset] + >>> bool(queryset) + >>> entry in queryset + >>> list(queryset) + +.. note:: + + Simply printing the queryset will not populate the cache. This is because + the call to ``__repr__()`` only returns a slice of the entire queryset. + .. _complex-lookups-with-q: Complex lookups with Q objects -- 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') 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 1fdc3d256d5d515bd0b4646ad57f231a2359e0cd Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 28 May 2013 11:54:53 -0400 Subject: Fixed #20523 - Incorrect form field for FilePathField. Thanks sane4ka.sh@ for the report. --- docs/topics/forms/modelforms.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 6a445432d2..4c46c6c0c0 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -75,7 +75,7 @@ Model field Form field ``FileField`` ``FileField`` -``FilePathField`` ``CharField`` +``FilePathField`` ``FilePathField`` ``FloatField`` ``FloatField`` -- cgit v1.3 From ded95ccdce0ba983c405ddde9eb071e454245a94 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 28 May 2013 15:11:41 -0400 Subject: Fixed #20484 -- Added model validation for GenericIPAddressField GenericIPAddressField must not allow blank for NOT NULL fields Thanks Erik Romijn. --- django/core/management/validation.py | 2 ++ docs/releases/1.6.txt | 7 +++++++ tests/invalid_models/invalid_models/models.py | 2 ++ tests/validation/models.py | 2 +- tests/validation/tests.py | 16 +++++++++++----- 5 files changed, 23 insertions(+), 6 deletions(-) (limited to 'docs') diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 2040a14582..a6d6a76985 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -113,6 +113,8 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": BooleanFields do not accept null values. Use a NullBooleanField instead.' % f.name) if isinstance(f, models.FilePathField) and not (f.allow_files or f.allow_folders): e.add(opts, '"%s": FilePathFields must have either allow_files or allow_folders set to True.' % f.name) + if isinstance(f, models.GenericIPAddressField) and not getattr(f, 'null', False) and getattr(f, 'blank', False): + e.add(opts, '"%s": GenericIPAddressField can not accept blank values if null values are not allowed, as blank values are stored as null.' % f.name) if f.choices: if isinstance(f.choices, six.string_types) or not is_iterable(f.choices): e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index c01128c684..efc1297265 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -601,6 +601,13 @@ Miscellaneous ``django.contrib.admindocs.middleware`` because it is an implementation detail of admindocs, proven not to be reusable in general. +* :class:`~django.db.models.GenericIPAddressField` will now only allow + ``blank`` values if ``null`` values are also allowed. Creating a + ``GenericIPAddressField`` where ``blank`` is allowed but ``null`` is not + will trigger a model validation error because ``blank`` values are always + stored as ``null``. Previously, storing a ``blank`` value in a field which + did not allow ``null`` would cause a database exception at runtime. + Features deprecated in 1.6 ========================== diff --git a/tests/invalid_models/invalid_models/models.py b/tests/invalid_models/invalid_models/models.py index 6ba3f04e5e..e1bac9cfb2 100644 --- a/tests/invalid_models/invalid_models/models.py +++ b/tests/invalid_models/invalid_models/models.py @@ -25,6 +25,7 @@ class FieldErrors(models.Model): index = models.CharField(max_length=10, db_index='bad') field_ = models.CharField(max_length=10) nullbool = models.BooleanField(null=True) + generic_ip_notnull_blank = models.GenericIPAddressField(null=False, blank=True) class Target(models.Model): @@ -380,6 +381,7 @@ invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-it 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. +invalid_models.fielderrors: "generic_ip_notnull_blank": GenericIPAddressField can not accept blank values if null values are not allowed, as blank values are stored as null. invalid_models.clash1: Accessor for field 'foreign' clashes with field 'Target.clash1_set'. Add a related_name argument to the definition for 'foreign'. invalid_models.clash1: Accessor for field 'foreign' clashes with related m2m field 'Target.clash1_set'. Add a related_name argument to the definition for 'foreign'. invalid_models.clash1: Reverse query name for field 'foreign' clashes with field 'Target.clash1'. Add a related_name argument to the definition for 'foreign'. diff --git a/tests/validation/models.py b/tests/validation/models.py index db083290fb..e95a1e0744 100644 --- a/tests/validation/models.py +++ b/tests/validation/models.py @@ -95,7 +95,7 @@ class GenericIPAddressTestModel(models.Model): blank=True, null=True) class GenericIPAddrUnpackUniqueTest(models.Model): - generic_v4unpack_ip = models.GenericIPAddressField(blank=True, unique=True, unpack_ipv4=True) + generic_v4unpack_ip = models.GenericIPAddressField(null=True, blank=True, unique=True, unpack_ipv4=True) # A model can't have multiple AutoFields diff --git a/tests/validation/tests.py b/tests/validation/tests.py index b571e0c298..9ddf796c2b 100644 --- a/tests/validation/tests.py +++ b/tests/validation/tests.py @@ -109,9 +109,9 @@ class GenericIPAddressFieldTests(ValidationTestCase): def test_correct_generic_ip_passes(self): giptm = GenericIPAddressTestModel(generic_ip="1.2.3.4") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) giptm = GenericIPAddressTestModel(generic_ip="2001::2") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) def test_invalid_generic_ip_raises_error(self): giptm = GenericIPAddressTestModel(generic_ip="294.4.2.1") @@ -121,7 +121,7 @@ class GenericIPAddressFieldTests(ValidationTestCase): def test_correct_v4_ip_passes(self): giptm = GenericIPAddressTestModel(v4_ip="1.2.3.4") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) def test_invalid_v4_ip_raises_error(self): giptm = GenericIPAddressTestModel(v4_ip="294.4.2.1") @@ -131,7 +131,7 @@ class GenericIPAddressFieldTests(ValidationTestCase): def test_correct_v6_ip_passes(self): giptm = GenericIPAddressTestModel(v6_ip="2001::2") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) def test_invalid_v6_ip_raises_error(self): giptm = GenericIPAddressTestModel(v6_ip="1.2.3.4") @@ -151,10 +151,16 @@ class GenericIPAddressFieldTests(ValidationTestCase): giptm = GenericIPAddressTestModel(generic_ip="::ffff:10.10.10.10") giptm.save() giptm = GenericIPAddressTestModel(generic_ip="10.10.10.10") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) # These two are the same, because we are doing IPv4 unpacking giptm = GenericIPAddrUnpackUniqueTest(generic_v4unpack_ip="::ffff:18.52.18.52") giptm.save() giptm = GenericIPAddrUnpackUniqueTest(generic_v4unpack_ip="18.52.18.52") self.assertFailsValidation(giptm.full_clean, ['generic_v4unpack_ip',]) + + def test_empty_generic_ip_passes(self): + giptm = GenericIPAddressTestModel(generic_ip="") + self.assertIsNone(giptm.full_clean()) + giptm = GenericIPAddressTestModel(generic_ip=None) + self.assertIsNone(giptm.full_clean()) -- 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') 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 01ae881bb4d13d6fe1b77f8758cc0bc72786cfaa Mon Sep 17 00:00:00 2001 From: Gavin Wahl Date: Wed, 29 May 2013 16:11:26 -0600 Subject: Don't hard-code class names when calling static methods normalize_email should be called on the instance, not the class. This has the same effect normally but is more helpful to subclassers. When methods are called directly on the class, subclasses can't override them. --- django/contrib/auth/models.py | 2 +- django/contrib/auth/tests/test_custom_user.py | 2 +- docs/topics/auth/customizing.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 5709d25d7f..798cc805a0 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -177,7 +177,7 @@ class UserManager(BaseUserManager): now = timezone.now() if not username: raise ValueError('The given username must be set') - email = UserManager.normalize_email(email) + email = self.normalize_email(email) user = self.model(username=username, email=email, is_staff=False, is_active=True, is_superuser=False, last_login=now, date_joined=now, **extra_fields) diff --git a/django/contrib/auth/tests/test_custom_user.py b/django/contrib/auth/tests/test_custom_user.py index 0d324f0953..a3a159880a 100644 --- a/django/contrib/auth/tests/test_custom_user.py +++ b/django/contrib/auth/tests/test_custom_user.py @@ -21,7 +21,7 @@ class CustomUserManager(BaseUserManager): raise ValueError('Users must have an email address') user = self.model( - email=CustomUserManager.normalize_email(email), + email=self.normalize_email(email), date_of_birth=date_of_birth, ) diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 9a8bab947c..bc021b14ad 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -939,7 +939,7 @@ authentication app:: raise ValueError('Users must have an email address') user = self.model( - email=MyUserManager.normalize_email(email), + email=self.normalize_email(email), date_of_birth=date_of_birth, ) -- 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') 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') 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') 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') 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') 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 f513764e72faf6bd4be1f063d2842cd4e5d76f7d Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 31 May 2013 07:43:05 -0400 Subject: Fixed #20515 - Clarified note regarding translator comments. Thanks wim@ for the suggestion. --- docs/releases/1.6.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 2afa9a32cd..91dda667f1 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -452,10 +452,9 @@ they are located after a ``{#`` / ``#}``-type comment on the same line. E.g.: Location of translator comments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Validation of the placement of :ref:`translator-comments-in-templates` -specified using ``{#`` / ``#}`` is now stricter. All translator comments not -located at the end of their respective lines in a template are ignored and a -warning is generated by :djadmin:`makemessages` when it finds them. E.g.: +:ref:`translator-comments-in-templates` specified using ``{#`` / ``#}`` need to +be at the end of a line. If they are not, the comments are ignored and +:djadmin:`makemessages` will generate a warning. For example: .. code-block:: html+django -- 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') 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 f10e9af22759cc287a967bba02c75d2834761f1e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 31 May 2013 08:18:29 -0400 Subject: Fixed #20166 - Corrected TEST_DEPENDENCIES example in testing docs. Thanks czambran. --- docs/topics/testing/advanced.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index cefb770469..b7f49d2b97 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -113,7 +113,8 @@ two databases. Controlling creation order for test databases --------------------------------------------- -By default, Django will always create the ``default`` database first. +By default, Django will assume all databases depend on the ``default`` +database and therefore always create the ``default`` database first. However, no guarantees are made on the creation order of any other databases in your test setup. @@ -129,6 +130,7 @@ can specify the dependencies that exist using the }, 'diamonds': { # ... db settings + 'TEST_DEPENDENCIES': [] }, 'clubs': { # ... db settings -- cgit v1.3 From 369b6fab25b55ceb363ba2a8cb7e0f1a88ef8f8d Mon Sep 17 00:00:00 2001 From: Anssi Kääriäinen Date: Thu, 30 May 2013 10:25:58 +0300 Subject: Fixed #18169 -- NoReverseMatch not silenced if from block.super --- django/core/urlresolvers.py | 3 +-- docs/releases/1.6.txt | 6 ++++++ tests/template_tests/templates/included_base.html | 3 +++ tests/template_tests/templates/included_content.html | 11 +++++++++++ tests/template_tests/tests.py | 9 +++++++++ 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 tests/template_tests/templates/included_base.html create mode 100644 tests/template_tests/templates/included_content.html (limited to 'docs') diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index 5f314ff490..d58f2a9fa3 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -75,8 +75,7 @@ class Resolver404(Http404): pass class NoReverseMatch(Exception): - # Don't make this raise an error when used in a template. - silent_variable_failure = True + pass def get_callable(lookup_view, can_fail=False): """ diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 91dda667f1..cd665a775e 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -616,6 +616,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. + Features deprecated in 1.6 ========================== diff --git a/tests/template_tests/templates/included_base.html b/tests/template_tests/templates/included_base.html new file mode 100644 index 0000000000..eae222cf9d --- /dev/null +++ b/tests/template_tests/templates/included_base.html @@ -0,0 +1,3 @@ +{% block content %} + {% block error_here %}{% endblock %} +{% endblock %} diff --git a/tests/template_tests/templates/included_content.html b/tests/template_tests/templates/included_content.html new file mode 100644 index 0000000000..bfc87c0425 --- /dev/null +++ b/tests/template_tests/templates/included_content.html @@ -0,0 +1,11 @@ +{% extends "included_base.html" %} + +{% block content %} + content + {{ block.super }} +{% endblock %} + +{% block error_here %} + error here + {% url "non_existing_url" %} +{% endblock %} diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 2aeaee9464..15668e00c0 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -444,6 +444,15 @@ class Templates(TestCase): output = template.render(Context({})) self.assertEqual(output, '1st time') + def test_super_errors(self): + """ + Test behavior of the raise errors into included blocks. + See #18169 + """ + t = loader.get_template('included_content.html') + with self.assertRaises(urlresolvers.NoReverseMatch): + t.render(Context({})) + def test_templates(self): template_tests = self.get_template_tests() filter_tests = filters.get_filter_tests() -- cgit v1.3 From f315693304865162d0b1bb8f913e9c5f14fb351e Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 1 Jun 2013 05:48:38 -0400 Subject: Fixed #20543 - Typo in db model fields docs. Thanks i.amber.jain@ for the report. --- docs/topics/db/models.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index baef01b6fb..c0ba53ddd7 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -182,7 +182,7 @@ ones: ('L', 'Large'), ) name = models.CharField(max_length=60) - shirt_size = models.CharField(max_length=2, choices=SHIRT_SIZES) + shirt_size = models.CharField(max_length=1, choices=SHIRT_SIZES) :: -- 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') 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 164c67c831c0d71ff30e7a4d1c5f5e6ed0d2f178 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Mon, 3 Jun 2013 15:59:20 +0800 Subject: Fixed #20552 -- Corrected release notes reference to name of new test discovery runner. --- docs/releases/1.6.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 2dca61bfed..08a574f211 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -78,10 +78,10 @@ location of tests. The previous runner ``models.py`` and ``tests.py`` modules of a Python package in :setting:`INSTALLED_APPS`. -The new runner (``django.test.runner.DjangoTestDiscoverRunner``) 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``. +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``. 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 -- 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') 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') 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 a35ed202419a054a9125ee6205a1a7b4eb3cb46d Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 4 Jun 2013 12:37:44 -0400 Subject: Fixed #18924 -- Made test.Client.logout send user_logged_out signal. Thanks awsum for the suggestion and Pavel Ponomarev and Florian Hahn for the patch. --- django/test/client.py | 18 ++++++--- docs/releases/1.6.txt | 4 ++ docs/topics/testing/overview.txt | 2 +- tests/test_client_regress/models.py | 12 ++++++ tests/test_client_regress/tests.py | 73 +++++++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 7 deletions(-) (limited to 'docs') diff --git a/django/test/client.py b/django/test/client.py index 46f55d7cdc..2ed0df8fea 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -13,7 +13,7 @@ except ImportError: # Python 2 from urlparse import urlparse, urlsplit from django.conf import settings -from django.contrib.auth import authenticate, login +from django.contrib.auth import authenticate, login, logout, get_user_model from django.core.handlers.base import BaseHandler from django.core.handlers.wsgi import WSGIRequest from django.core.signals import (request_started, request_finished, @@ -571,11 +571,17 @@ class Client(RequestFactory): Causes the authenticated user to be logged out. """ - session = import_module(settings.SESSION_ENGINE).SessionStore() - session_cookie = self.cookies.get(settings.SESSION_COOKIE_NAME) - if session_cookie: - session.delete(session_key=session_cookie.value) - self.cookies = SimpleCookie() + request = HttpRequest() + engine = import_module(settings.SESSION_ENGINE) + UserModel = get_user_model() + if self.session: + request.session = self.session + uid = self.session.get("_auth_user_id") + if uid: + request.user = UserModel._default_manager.get(pk=uid) + else: + request.session = engine.SessionStore() + logout(request) def _handle_redirects(self, response, **extra): "Follows any redirects by requesting responses from the server using GET." diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 08a574f211..0970453b18 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -623,6 +623,10 @@ Miscellaneous raises NoReverseMatch. There is no change to {% 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 + :func:`~django.contrib.auth.signals.user_logged_out` signal. + Features deprecated in 1.6 ========================== diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 470051e78e..d81b171a49 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -696,7 +696,7 @@ Use the ``django.test.client.Client`` class to make requests. After you call this method, the test client will have all the cookies and session data cleared to defaults. Subsequent requests will appear - to come from an AnonymousUser. + to come from an :class:`~django.contrib.auth.models.AnonymousUser`. Testing responses ~~~~~~~~~~~~~~~~~ diff --git a/tests/test_client_regress/models.py b/tests/test_client_regress/models.py index e69de29bb2..b72d4480bf 100644 --- a/tests/test_client_regress/models.py +++ b/tests/test_client_regress/models.py @@ -0,0 +1,12 @@ +from django.db import models +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager + + +class CustomUser(AbstractBaseUser): + email = models.EmailField(verbose_name='email address', max_length=255, unique=True) + custom_objects = BaseUserManager() + + USERNAME_FIELD = 'email' + + class Meta: + app_label = 'test_client_regress' diff --git a/tests/test_client_regress/tests.py b/tests/test_client_regress/tests.py index dc656c127d..67e66fa52d 100644 --- a/tests/test_client_regress/tests.py +++ b/tests/test_client_regress/tests.py @@ -17,7 +17,10 @@ from django.template.response import SimpleTemplateResponse from django.utils._os import upath from django.utils.translation import ugettext_lazy from django.http import HttpResponse +from django.contrib.auth.signals import user_logged_out, user_logged_in +from django.contrib.auth.models import User +from .models import CustomUser from .views import CustomTestException @override_settings( @@ -961,6 +964,76 @@ class SessionTests(TestCase): self.client.logout() self.client.logout() + def test_logout_with_user(self): + """Logout should send user_logged_out signal if user was logged in.""" + def listener(*args, **kwargs): + listener.executed = True + self.assertEqual(kwargs['sender'], User) + listener.executed = False + + user_logged_out.connect(listener) + self.client.login(username='testclient', password='password') + self.client.logout() + user_logged_out.disconnect(listener) + self.assertTrue(listener.executed) + + @override_settings(AUTH_USER_MODEL='test_client_regress.CustomUser') + def test_logout_with_custom_user(self): + """Logout should send user_logged_out signal if custom user was logged in.""" + def listener(*args, **kwargs): + self.assertEqual(kwargs['sender'], CustomUser) + listener.executed = True + listener.executed = False + u = CustomUser.custom_objects.create(email='test@test.com') + u.set_password('password') + u.save() + + user_logged_out.connect(listener) + self.client.login(username='test@test.com', password='password') + self.client.logout() + user_logged_out.disconnect(listener) + self.assertTrue(listener.executed) + + def test_logout_without_user(self): + """Logout should send signal even if user not authenticated.""" + def listener(user, *args, **kwargs): + listener.user = user + listener.executed = True + listener.executed = False + + user_logged_out.connect(listener) + self.client.login(username='incorrect', password='password') + self.client.logout() + user_logged_out.disconnect(listener) + + self.assertTrue(listener.executed) + self.assertIsNone(listener.user) + + def test_login_with_user(self): + """Login should send user_logged_in signal on successful login.""" + def listener(*args, **kwargs): + listener.executed = True + listener.executed = False + + user_logged_in.connect(listener) + self.client.login(username='testclient', password='password') + user_logged_out.disconnect(listener) + + self.assertTrue(listener.executed) + + def test_login_without_signal(self): + """Login shouldn't send signal if user wasn't logged in""" + def listener(*args, **kwargs): + listener.executed = True + listener.executed = False + + user_logged_in.connect(listener) + self.client.login(username='incorrect', password='password') + user_logged_in.disconnect(listener) + + self.assertFalse(listener.executed) + + class RequestMethodTests(TestCase): def test_get(self): "Request a view via request method GET" -- 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') 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') 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') 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