summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Godwin <andrew@aeracode.org>2012-12-18 09:02:07 +0000
committerAndrew Godwin <andrew@aeracode.org>2012-12-18 09:02:07 +0000
commitb62e82365ad56ca930f7abb1d1dbdf9ce5a7c7c3 (patch)
tree76bce44f4c7ee6172dcf116be6e4a26e380ef20a
parent6a632e04578776e877adc5e2dc53f008c890a0d4 (diff)
parentc64b57d16688025b2d48668d5c4cb9eda7484612 (diff)
Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts: django/db/models/loading.py django/db/models/options.py
-rw-r--r--.gitattributes2
-rw-r--r--AUTHORS6
-rw-r--r--django/__init__.py2
-rwxr-xr-xdjango/bin/daily_cleanup.py8
-rw-r--r--django/conf/__init__.py14
-rw-r--r--django/conf/global_settings.py9
-rw-r--r--django/conf/locale/__init__.py15
-rw-r--r--django/conf/locale/ca/formats.py4
-rw-r--r--django/conf/locale/cs/formats.py25
-rw-r--r--django/conf/locale/da/formats.py4
-rw-r--r--django/conf/locale/de/formats.py8
-rw-r--r--django/conf/locale/de_CH/formats.py8
-rw-r--r--django/conf/locale/en/formats.py5
-rw-r--r--django/conf/locale/en_GB/formats.py5
-rw-r--r--django/conf/locale/es/formats.py4
-rw-r--r--django/conf/locale/es_AR/formats.py4
-rw-r--r--django/conf/locale/es_MX/formats.py3
-rw-r--r--django/conf/locale/es_NI/formats.py3
-rw-r--r--django/conf/locale/fr/formats.py8
-rw-r--r--django/conf/locale/hr/formats.py5
-rw-r--r--django/conf/locale/it/formats.py4
-rw-r--r--django/conf/locale/ka/formats.py5
-rw-r--r--django/conf/locale/ko/formats.py1
-rw-r--r--django/conf/locale/lv/formats.py1
-rw-r--r--django/conf/locale/mk/formats.py9
-rw-r--r--django/conf/locale/ml/formats.py5
-rw-r--r--django/conf/locale/nb/formats.py7
-rw-r--r--django/conf/locale/nl/formats.py7
-rw-r--r--django/conf/locale/nn/formats.py6
-rw-r--r--django/conf/locale/pl/formats.py9
-rw-r--r--django/conf/locale/pt/formats.py5
-rw-r--r--django/conf/locale/pt_BR/formats.py9
-rw-r--r--django/conf/locale/ru/formats.py8
-rw-r--r--django/conf/locale/sk/formats.py9
-rw-r--r--django/conf/locale/sl/formats.py5
-rw-r--r--django/conf/locale/sr/formats.py8
-rw-r--r--django/conf/locale/sr_Latn/formats.py8
-rw-r--r--django/conf/locale/sv/formats.py5
-rw-r--r--django/conf/locale/tr/formats.py9
-rw-r--r--django/conf/project_template/project_name/settings.py4
-rw-r--r--django/contrib/admin/filters.py4
-rw-r--r--django/contrib/admin/forms.py12
-rw-r--r--django/contrib/admin/helpers.py4
-rw-r--r--django/contrib/admin/models.py13
-rw-r--r--django/contrib/admin/options.py41
-rw-r--r--django/contrib/admin/sites.py10
-rw-r--r--django/contrib/admin/static/admin/css/base.css4
-rw-r--r--django/contrib/admin/static/admin/css/rtl.css5
-rw-r--r--django/contrib/admin/static/admin/css/widgets.css15
-rw-r--r--django/contrib/admin/static/admin/js/admin/ordering.js137
-rw-r--r--django/contrib/admin/static/admin/js/getElementsBySelector.js167
-rw-r--r--django/contrib/admin/templates/admin/auth/user/change_password.html4
-rw-r--r--django/contrib/admin/templates/admin/change_form.html2
-rw-r--r--django/contrib/admin/templates/admin/edit_inline/tabular.html2
-rw-r--r--django/contrib/admin/templates/admin/includes/fieldset.html2
-rw-r--r--django/contrib/admin/templates/admin/index.html4
-rw-r--r--django/contrib/admin/templates/admin/submit_line.html8
-rw-r--r--django/contrib/admin/templatetags/admin_modify.py2
-rw-r--r--django/contrib/admin/templatetags/adminmedia.py15
-rw-r--r--django/contrib/admin/widgets.py15
-rw-r--r--django/contrib/admindocs/views.py3
-rw-r--r--django/contrib/auth/__init__.py9
-rw-r--r--django/contrib/auth/admin.py8
-rw-r--r--django/contrib/auth/context_processors.py4
-rw-r--r--django/contrib/auth/forms.py27
-rw-r--r--django/contrib/auth/handlers/modwsgi.py7
-rw-r--r--django/contrib/auth/management/__init__.py20
-rw-r--r--django/contrib/auth/middleware.py17
-rw-r--r--django/contrib/auth/models.py160
-rw-r--r--django/contrib/auth/tests/__init__.py13
-rw-r--r--django/contrib/auth/tests/auth_backends.py87
-rw-r--r--django/contrib/auth/tests/basic.py2
-rw-r--r--django/contrib/auth/tests/context_processors.py3
-rw-r--r--django/contrib/auth/tests/custom_user.py58
-rw-r--r--django/contrib/auth/tests/forms.py28
-rw-r--r--django/contrib/auth/tests/handlers.py43
-rw-r--r--django/contrib/auth/tests/models.py34
-rw-r--r--django/contrib/auth/tests/remote_user.py25
-rw-r--r--django/contrib/auth/tests/templates/context_processors/auth_attrs_user.html2
-rw-r--r--django/contrib/auth/tests/views.py11
-rw-r--r--django/contrib/auth/tokens.py2
-rw-r--r--django/contrib/auth/views.py52
-rw-r--r--django/contrib/comments/__init__.py4
-rw-r--r--django/contrib/comments/signals.py2
-rw-r--r--django/contrib/comments/views/comments.py8
-rw-r--r--django/contrib/comments/views/moderation.py10
-rw-r--r--django/contrib/comments/views/utils.py17
-rw-r--r--django/contrib/contenttypes/generic.py38
-rw-r--r--django/contrib/contenttypes/management.py16
-rw-r--r--django/contrib/flatpages/forms.py4
-rw-r--r--django/contrib/formtools/tests/__init__.py5
-rw-r--r--django/contrib/formtools/tests/wizard/wizardtests/tests.py19
-rw-r--r--django/contrib/formtools/wizard/storage/base.py8
-rw-r--r--django/contrib/formtools/wizard/views.py14
-rw-r--r--django/contrib/gis/db/backends/oracle/compiler.py24
-rw-r--r--django/contrib/gis/db/backends/oracle/operations.py9
-rw-r--r--django/contrib/gis/db/backends/postgis/creation.py29
-rw-r--r--django/contrib/gis/db/backends/spatialite/base.py46
-rw-r--r--django/contrib/gis/db/models/query.py6
-rw-r--r--django/contrib/gis/db/models/sql/compiler.py8
-rw-r--r--django/contrib/gis/geoip/tests.py1
-rw-r--r--django/contrib/gis/geometry/test_data.py3
-rw-r--r--django/contrib/gis/geos/mutable_list.py23
-rw-r--r--django/contrib/gis/geos/tests/test_geos.py15
-rw-r--r--django/contrib/gis/geos/tests/test_mutable_list.py9
-rw-r--r--django/contrib/gis/measure.py16
-rw-r--r--django/contrib/gis/tests/geo3d/tests.py3
-rw-r--r--django/contrib/gis/tests/geogapp/tests.py3
-rw-r--r--django/contrib/gis/tests/layermap/tests.py3
-rw-r--r--django/contrib/gis/tests/utils.py2
-rw-r--r--django/contrib/gis/utils/ogrinspect.py2
-rw-r--r--django/contrib/humanize/tests.py13
-rw-r--r--django/contrib/localflavor/ar/forms.py7
-rw-r--r--django/contrib/redirects/tests.py42
-rw-r--r--django/contrib/sessions/backends/base.py67
-rw-r--r--django/contrib/sessions/backends/cache.py13
-rw-r--r--django/contrib/sessions/backends/cached_db.py25
-rw-r--r--django/contrib/sessions/backends/db.py7
-rw-r--r--django/contrib/sessions/backends/file.py75
-rw-r--r--django/contrib/sessions/backends/signed_cookies.py5
-rw-r--r--django/contrib/sessions/management/__init__.py (renamed from tests/modeltests/pagination/__init__.py)0
-rw-r--r--django/contrib/sessions/management/commands/__init__.py (renamed from tests/regressiontests/pagination_regress/__init__.py)0
-rw-r--r--django/contrib/sessions/management/commands/clearsessions.py15
-rw-r--r--django/contrib/sessions/tests.py161
-rw-r--r--django/contrib/sitemaps/tests/http.py5
-rw-r--r--django/contrib/staticfiles/storage.py3
-rw-r--r--django/core/cache/__init__.py10
-rw-r--r--django/core/cache/backends/base.py12
-rw-r--r--django/core/cache/backends/db.py12
-rw-r--r--django/core/cache/backends/memcached.py6
-rw-r--r--django/core/files/base.py17
-rw-r--r--django/core/handlers/base.py2
-rw-r--r--django/core/handlers/wsgi.py24
-rw-r--r--django/core/mail/backends/smtp.py5
-rw-r--r--django/core/management/__init__.py18
-rw-r--r--django/core/management/commands/cleanup.py16
-rw-r--r--django/core/management/commands/compilemessages.py5
-rw-r--r--django/core/management/commands/loaddata.py278
-rw-r--r--django/core/management/commands/makemessages.py6
-rw-r--r--django/core/management/sql.py13
-rw-r--r--django/core/management/templates.py8
-rw-r--r--django/core/management/validation.py60
-rw-r--r--django/core/paginator.py77
-rw-r--r--django/core/serializers/base.py4
-rw-r--r--django/core/serializers/xml_serializer.py2
-rw-r--r--django/core/servers/basehttp.py4
-rw-r--r--django/core/signing.py16
-rw-r--r--django/core/urlresolvers.py14
-rw-r--r--django/db/__init__.py2
-rw-r--r--django/db/backends/__init__.py15
-rw-r--r--django/db/backends/creation.py52
-rw-r--r--django/db/backends/mysql/base.py82
-rw-r--r--django/db/backends/oracle/base.py132
-rw-r--r--django/db/backends/oracle/creation.py10
-rw-r--r--django/db/backends/postgresql_psycopg2/base.py91
-rw-r--r--django/db/backends/sqlite3/base.py32
-rw-r--r--django/db/backends/util.py8
-rw-r--r--django/db/models/base.py5
-rw-r--r--django/db/models/expressions.py12
-rw-r--r--django/db/models/fields/related.py74
-rw-r--r--django/db/models/loading.py20
-rw-r--r--django/db/models/options.py60
-rw-r--r--django/db/models/query.py117
-rw-r--r--django/db/models/signals.py17
-rw-r--r--django/db/models/sql/compiler.py191
-rw-r--r--django/db/models/sql/constants.py17
-rw-r--r--django/db/models/sql/expressions.py13
-rw-r--r--django/db/models/sql/query.py774
-rw-r--r--django/db/models/sql/subqueries.py4
-rw-r--r--django/db/utils.py12
-rw-r--r--django/dispatch/dispatcher.py73
-rw-r--r--django/dispatch/saferef.py16
-rw-r--r--django/forms/formsets.py9
-rw-r--r--django/forms/models.py2
-rw-r--r--django/forms/widgets.py4
-rw-r--r--django/http/multipartparser.py24
-rw-r--r--django/http/request.py6
-rw-r--r--django/http/response.py4
-rw-r--r--django/middleware/cache.py1
-rw-r--r--django/middleware/common.py13
-rw-r--r--django/shortcuts/__init__.py12
-rw-r--r--django/template/defaultfilters.py2
-rw-r--r--django/template/defaulttags.py13
-rw-r--r--django/template/response.py2
-rw-r--r--django/test/signals.py7
-rw-r--r--django/test/testcases.py116
-rw-r--r--django/test/utils.py8
-rw-r--r--django/utils/_os.py23
-rw-r--r--django/utils/autoreload.py42
-rw-r--r--django/utils/datastructures.py33
-rw-r--r--django/utils/encoding.py2
-rw-r--r--django/utils/formats.py16
-rw-r--r--django/utils/functional.py10
-rw-r--r--django/utils/html.py19
-rw-r--r--django/utils/http.py12
-rw-r--r--django/utils/log.py16
-rw-r--r--django/utils/translation/trans_real.py10
-rw-r--r--django/utils/tree.py4
-rw-r--r--django/views/generic/base.py11
-rw-r--r--django/views/generic/edit.py4
-rw-r--r--django/views/generic/list.py30
-rw-r--r--django/views/i18n.py22
-rw-r--r--docs/Makefile6
-rw-r--r--docs/_theme/djangodocs/static/djangodocs.css5
-rw-r--r--docs/_theme/djangodocs/static/docicons-warning.pngbin0 -> 782 bytes
-rw-r--r--docs/conf.py35
-rw-r--r--docs/faq/admin.txt23
-rw-r--r--docs/faq/general.txt2
-rw-r--r--docs/faq/install.txt8
-rw-r--r--docs/faq/troubleshooting.txt37
-rw-r--r--docs/howto/custom-file-storage.txt19
-rw-r--r--docs/howto/deployment/wsgi/modwsgi.txt16
-rw-r--r--docs/index.txt7
-rw-r--r--docs/internals/contributing/writing-code/submitting-patches.txt7
-rw-r--r--docs/internals/contributing/writing-code/unit-tests.txt7
-rw-r--r--docs/internals/deprecation.txt17
-rw-r--r--docs/intro/contributing.txt580
-rw-r--r--docs/intro/index.txt15
-rw-r--r--docs/intro/overview.txt3
-rw-r--r--docs/intro/reusable-apps.txt365
-rw-r--r--docs/intro/tutorial01.txt38
-rw-r--r--docs/intro/tutorial02.txt5
-rw-r--r--docs/intro/tutorial03.txt14
-rw-r--r--docs/intro/tutorial04.txt7
-rw-r--r--docs/intro/tutorial05.txt650
-rw-r--r--docs/make.bat9
-rw-r--r--docs/misc/design-philosophies.txt3
-rw-r--r--docs/ref/class-based-views/flattened-index.txt7
-rw-r--r--docs/ref/class-based-views/generic-date-based.txt299
-rw-r--r--docs/ref/class-based-views/generic-editing.txt7
-rw-r--r--docs/ref/class-based-views/index.txt5
-rw-r--r--docs/ref/class-based-views/mixins-multiple-object.txt31
-rw-r--r--docs/ref/contrib/admin/actions.txt9
-rw-r--r--docs/ref/contrib/admin/index.txt101
-rw-r--r--docs/ref/contrib/comments/index.txt9
-rw-r--r--docs/ref/contrib/comments/signals.txt4
-rw-r--r--docs/ref/contrib/comments/upgrade.txt78
-rw-r--r--docs/ref/contrib/formtools/form-wizard.txt32
-rw-r--r--docs/ref/contrib/gis/geos.txt11
-rw-r--r--docs/ref/contrib/gis/install/index.txt2
-rw-r--r--docs/ref/contrib/gis/tutorial.txt281
-rw-r--r--docs/ref/contrib/messages.txt2
-rw-r--r--docs/ref/contrib/sites.txt29
-rw-r--r--docs/ref/contrib/syndication.txt5
-rw-r--r--docs/ref/databases.txt5
-rw-r--r--docs/ref/django-admin.txt42
-rw-r--r--docs/ref/forms/fields.txt2
-rw-r--r--docs/ref/forms/widgets.txt192
-rw-r--r--docs/ref/models/fields.txt7
-rw-r--r--docs/ref/models/instances.txt81
-rw-r--r--docs/ref/models/options.txt15
-rw-r--r--docs/ref/models/querysets.txt40
-rw-r--r--docs/ref/settings.txt67
-rw-r--r--docs/ref/templates/builtins.txt70
-rw-r--r--docs/ref/unicode.txt8
-rw-r--r--docs/ref/urlresolvers.txt26
-rw-r--r--docs/ref/utils.txt12
-rw-r--r--docs/releases/1.0-beta-2.txt4
-rw-r--r--docs/releases/1.0-porting-guide.txt4
-rw-r--r--docs/releases/1.0.txt4
-rw-r--r--docs/releases/1.5-alpha-1.txt24
-rw-r--r--docs/releases/1.5-beta-1.txt702
-rw-r--r--docs/releases/1.5.txt100
-rw-r--r--docs/releases/1.6.txt43
-rw-r--r--docs/releases/index.txt8
-rw-r--r--docs/topics/_images/django_unittest_classes_hierarchy.graffle883
-rw-r--r--docs/topics/_images/django_unittest_classes_hierarchy.pdfbin0 -> 51979 bytes
-rw-r--r--docs/topics/_images/django_unittest_classes_hierarchy.pngbin11757 -> 0 bytes
-rw-r--r--docs/topics/_images/django_unittest_classes_hierarchy.svg3
-rw-r--r--docs/topics/auth.txt163
-rw-r--r--docs/topics/cache.txt10
-rw-r--r--docs/topics/class-based-views/generic-editing.txt12
-rw-r--r--docs/topics/class-based-views/index.txt20
-rw-r--r--docs/topics/db/managers.txt15
-rw-r--r--docs/topics/db/models.txt7
-rw-r--r--docs/topics/db/multi-db.txt46
-rw-r--r--docs/topics/db/optimization.txt35
-rw-r--r--docs/topics/db/queries.txt21
-rw-r--r--docs/topics/db/transactions.txt12
-rw-r--r--docs/topics/files.txt2
-rw-r--r--docs/topics/forms/modelforms.txt15
-rw-r--r--docs/topics/http/_images/middleware.graffle957
-rw-r--r--docs/topics/http/_images/middleware.pdfbin0 -> 41900 bytes
-rw-r--r--docs/topics/http/_images/middleware.pngbin56301 -> 0 bytes
-rw-r--r--docs/topics/http/_images/middleware.svg3
-rw-r--r--docs/topics/http/middleware.txt191
-rw-r--r--docs/topics/http/sessions.txt52
-rw-r--r--docs/topics/http/shortcuts.txt2
-rw-r--r--docs/topics/http/urls.txt15
-rw-r--r--docs/topics/http/views.txt11
-rw-r--r--docs/topics/install.txt34
-rw-r--r--docs/topics/logging.txt23
-rw-r--r--docs/topics/python3.txt13
-rw-r--r--docs/topics/security.txt25
-rw-r--r--docs/topics/serialization.txt9
-rw-r--r--docs/topics/templates.txt6
-rw-r--r--docs/topics/testing.txt47
-rw-r--r--tests/modeltests/basic/tests.py36
-rw-r--r--tests/modeltests/expressions/tests.py43
-rw-r--r--tests/modeltests/field_subclassing/tests.py3
-rw-r--r--tests/modeltests/fixtures/tests.py26
-rw-r--r--tests/modeltests/generic_relations/tests.py6
-rw-r--r--tests/modeltests/get_object_or_404/tests.py25
-rw-r--r--tests/modeltests/invalid_models/invalid_models/models.py8
-rw-r--r--tests/modeltests/m2m_recursive/tests.py27
-rw-r--r--tests/modeltests/many_to_one/tests.py8
-rw-r--r--tests/modeltests/model_forms/models.py9
-rw-r--r--tests/modeltests/model_forms/tests.py69
-rw-r--r--tests/modeltests/pagination/tests.py134
-rw-r--r--tests/modeltests/prefetch_related/models.py10
-rw-r--r--tests/modeltests/prefetch_related/tests.py10
-rw-r--r--tests/modeltests/proxy_model_inheritance/tests.py3
-rw-r--r--tests/modeltests/raw_query/models.py2
-rw-r--r--tests/modeltests/str/models.py8
-rw-r--r--tests/modeltests/str/tests.py12
-rw-r--r--tests/regressiontests/admin_changelist/admin.py12
-rw-r--r--tests/regressiontests/admin_changelist/tests.py25
-rw-r--r--tests/regressiontests/admin_filters/tests.py58
-rw-r--r--tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-19397-binary-file.icobin0 -> 894 bytes
-rw-r--r--tests/regressiontests/admin_scripts/tests.py65
-rw-r--r--tests/regressiontests/admin_scripts/urls.py3
-rw-r--r--tests/regressiontests/admin_util/tests.py4
-rw-r--r--tests/regressiontests/admin_views/admin.py46
-rw-r--r--tests/regressiontests/admin_views/customadmin.py9
-rw-r--r--tests/regressiontests/admin_views/models.py8
-rw-r--r--tests/regressiontests/admin_views/tests.py116
-rw-r--r--tests/regressiontests/admin_views/urls.py1
-rw-r--r--tests/regressiontests/admin_widgets/models.py1
-rw-r--r--tests/regressiontests/admin_widgets/tests.py50
-rw-r--r--tests/regressiontests/aggregation_regress/tests.py95
-rw-r--r--tests/regressiontests/app_loading/tests.py3
-rw-r--r--tests/regressiontests/backends/tests.py40
-rw-r--r--tests/regressiontests/bug639/tests.py3
-rw-r--r--tests/regressiontests/bulk_create/tests.py16
-rw-r--r--tests/regressiontests/cache/tests.py6
-rw-r--r--tests/regressiontests/comment_tests/tests/app_api_tests.py27
-rw-r--r--tests/regressiontests/comment_tests/tests/comment_view_tests.py7
-rw-r--r--tests/regressiontests/comment_tests/tests/feed_tests.py23
-rw-r--r--tests/regressiontests/comment_tests/tests/moderation_view_tests.py90
-rw-r--r--tests/regressiontests/defaultfilters/tests.py24
-rw-r--r--tests/regressiontests/defer_regress/models.py4
-rw-r--r--tests/regressiontests/defer_regress/tests.py31
-rw-r--r--tests/regressiontests/delete_regress/tests.py16
-rw-r--r--tests/regressiontests/expressions_regress/tests.py37
-rw-r--r--tests/regressiontests/extra_regress/tests.py9
-rw-r--r--tests/regressiontests/file_storage/tests.py27
-rw-r--r--tests/regressiontests/file_uploads/models.py9
-rw-r--r--tests/regressiontests/file_uploads/tests.py62
-rw-r--r--tests/regressiontests/file_uploads/views.py4
-rw-r--r--tests/regressiontests/fixtures_regress/tests.py8
-rw-r--r--tests/regressiontests/forms/models.py6
-rw-r--r--tests/regressiontests/forms/templates/forms/article_form.html8
-rw-r--r--tests/regressiontests/forms/tests/__init__.py2
-rw-r--r--tests/regressiontests/forms/tests/fields.py11
-rw-r--r--tests/regressiontests/forms/tests/forms.py5
-rw-r--r--tests/regressiontests/forms/tests/formsets.py21
-rw-r--r--tests/regressiontests/forms/tests/input_formats.py31
-rw-r--r--tests/regressiontests/forms/tests/widgets.py24
-rw-r--r--tests/regressiontests/forms/urls.py9
-rw-r--r--tests/regressiontests/forms/views.py8
-rw-r--r--tests/regressiontests/generic_views/base.py11
-rw-r--r--tests/regressiontests/generic_views/edit.py9
-rw-r--r--tests/regressiontests/generic_views/forms.py5
-rw-r--r--tests/regressiontests/generic_views/list.py41
-rw-r--r--tests/regressiontests/generic_views/models.py4
-rw-r--r--tests/regressiontests/generic_views/tests.py4
-rw-r--r--tests/regressiontests/generic_views/urls.py8
-rw-r--r--tests/regressiontests/generic_views/views.py11
-rw-r--r--tests/regressiontests/httpwrappers/tests.py11
-rw-r--r--tests/regressiontests/i18n/commands/compilation.py7
-rw-r--r--tests/regressiontests/i18n/commands/extraction.py31
-rw-r--r--tests/regressiontests/i18n/contenttypes/tests.py3
-rw-r--r--tests/regressiontests/i18n/patterns/tests.py5
-rw-r--r--tests/regressiontests/i18n/tests.py11
-rw-r--r--tests/regressiontests/indexes/__init__.py0
-rw-r--r--tests/regressiontests/indexes/models.py11
-rw-r--r--tests/regressiontests/indexes/tests.py12
-rw-r--r--tests/regressiontests/initial_sql_regress/sql/simple.sql3
-rw-r--r--tests/regressiontests/initial_sql_regress/tests.py7
-rw-r--r--tests/regressiontests/inspectdb/tests.py11
-rw-r--r--tests/regressiontests/introspection/models.py4
-rw-r--r--tests/regressiontests/introspection/tests.py27
-rw-r--r--tests/regressiontests/logging_tests/logconfig.py7
-rw-r--r--tests/regressiontests/logging_tests/tests.py83
-rw-r--r--tests/regressiontests/m2m_regress/models.py17
-rw-r--r--tests/regressiontests/m2m_regress/tests.py10
-rw-r--r--tests/regressiontests/m2m_through_regress/models.py11
-rw-r--r--tests/regressiontests/m2m_through_regress/tests.py97
-rw-r--r--tests/regressiontests/mail/tests.py17
-rw-r--r--tests/regressiontests/managers_regress/tests.py3
-rw-r--r--tests/regressiontests/middleware/tests.py9
-rw-r--r--tests/regressiontests/model_fields/imagefield.py5
-rw-r--r--tests/regressiontests/model_forms_regress/models.py3
-rw-r--r--tests/regressiontests/model_formsets_regress/tests.py10
-rw-r--r--tests/regressiontests/model_regress/tests.py9
-rw-r--r--tests/regressiontests/multiple_database/tests.py58
-rw-r--r--tests/regressiontests/pagination/__init__.py0
-rw-r--r--tests/regressiontests/pagination/custom.py20
-rw-r--r--tests/regressiontests/pagination/models.py (renamed from tests/modeltests/pagination/models.py)8
-rw-r--r--tests/regressiontests/pagination/tests.py (renamed from tests/regressiontests/pagination_regress/tests.py)147
-rw-r--r--tests/regressiontests/pagination_regress/models.py1
-rw-r--r--tests/regressiontests/queries/models.py23
-rw-r--r--tests/regressiontests/queries/tests.py326
-rw-r--r--tests/regressiontests/requests/tests.py37
-rw-r--r--tests/regressiontests/select_related_onetoone/models.py42
-rw-r--r--tests/regressiontests/select_related_onetoone/tests.py102
-rw-r--r--tests/regressiontests/serializers_regress/models.py13
-rw-r--r--tests/regressiontests/serializers_regress/tests.py15
-rw-r--r--tests/regressiontests/servers/tests.py3
-rw-r--r--tests/regressiontests/settings_tests/tests.py26
-rw-r--r--tests/regressiontests/staticfiles_tests/tests.py29
-rw-r--r--tests/regressiontests/swappable_models/__init__.py0
-rw-r--r--tests/regressiontests/swappable_models/models.py15
-rw-r--r--tests/regressiontests/swappable_models/tests.py46
-rw-r--r--tests/regressiontests/templates/filters.py5
-rw-r--r--tests/regressiontests/templates/loaders.py7
-rw-r--r--tests/regressiontests/templates/response.py18
-rw-r--r--tests/regressiontests/templates/tests.py30
-rw-r--r--tests/regressiontests/test_client_regress/tests.py5
-rw-r--r--tests/regressiontests/test_utils/models.py6
-rw-r--r--tests/regressiontests/test_utils/tests.py40
-rw-r--r--tests/regressiontests/transactions_regress/tests.py27
-rw-r--r--tests/regressiontests/urlpatterns_reverse/tests.py14
-rw-r--r--tests/regressiontests/utils/archive.py3
-rw-r--r--tests/regressiontests/utils/feedgenerator.py8
-rw-r--r--tests/regressiontests/utils/html.py3
-rw-r--r--tests/regressiontests/utils/module_loading.py3
-rw-r--r--tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.mobin0 -> 615 bytes
-rw-r--r--tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.po40
-rw-r--r--tests/regressiontests/views/templates/jsi18n.html44
-rw-r--r--tests/regressiontests/views/tests/__init__.py2
-rw-r--r--tests/regressiontests/views/tests/debug.py46
-rw-r--r--tests/regressiontests/views/tests/i18n.py72
-rw-r--r--tests/regressiontests/views/urls.py10
-rw-r--r--tests/regressiontests/views/views.py3
-rwxr-xr-xtests/runtests.py11
436 files changed, 11658 insertions, 3595 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..49cb19117c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Normalize line endings to avoid spurious failures in the core test suite on Windows.
+*html text eol=lf \ No newline at end of file
diff --git a/AUTHORS b/AUTHORS
index 5799b941ff..0f793cc5f4 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -91,6 +91,7 @@ answer newbie questions, and generally made Django that much better:
James Bennett
Danilo Bargen
Shai Berger <shai@platonix.com>
+ berto
Julian Bez
Arvis Bickovskis <viestards.lists@gmail.com>
Natalia Bidart <nataliabidart@gmail.com>
@@ -231,6 +232,7 @@ answer newbie questions, and generally made Django that much better:
Simon Greenhill <dev@simon.net.nz>
Owen Griffiths
Espen Grindhaug <http://grindhaug.org/>
+ Mike Grouchy <http://mikegrouchy.com/>
Janos Guljas
Thomas Güttler <hv@tbz-pariv.de>
Horst Gutmann <zerok@zerokspot.com>
@@ -380,6 +382,7 @@ answer newbie questions, and generally made Django that much better:
Christian Metts
michal@plovarna.cz
Slawek Mikula <slawek dot mikula at gmail dot com>
+ Katie Miller <katie@sub50.com>
Shawn Milochik <shawn@milochik.com>
mitakummaa@gmail.com
Taylor Mitchell <taylor.mitchell@gmail.com>
@@ -510,6 +513,7 @@ answer newbie questions, and generally made Django that much better:
Johan C. Stöver <johan@nilling.nl>
Nowell Strite <http://nowell.strite.org/>
Thomas Stromberg <tstromberg@google.com>
+ Ben Sturmfels <ben@sturm.com.au>
Travis Swicegood <travis@domain51.com>
Pascal Varet
SuperJared
@@ -528,6 +532,7 @@ answer newbie questions, and generally made Django that much better:
Terry Huang <terryh.tp@gmail.com>
Travis Terry <tdterry7@gmail.com>
thebjorn <bp@datakortet.no>
+ Lowe Thiderman <lowe.thiderman@gmail.com>
Zach Thompson <zthompson47@gmail.com>
Michael Thornhill <michael.thornhill@gmail.com>
Deepak Thukral <deep.thukral@gmail.com>
@@ -585,6 +590,7 @@ answer newbie questions, and generally made Django that much better:
Gasper Zejn <zejn@kiberpipa.org>
Jarek Zgoda <jarek.zgoda@gmail.com>
Cheng Zhang
+ Hannes Struß <x@hannesstruss.de>
A big THANK YOU goes to:
diff --git a/django/__init__.py b/django/__init__.py
index 32e1374765..873c328add 100644
--- a/django/__init__.py
+++ b/django/__init__.py
@@ -1,4 +1,4 @@
-VERSION = (1, 5, 0, 'alpha', 0)
+VERSION = (1, 6, 0, 'alpha', 0)
def get_version(*args, **kwargs):
# Don't litter django/__init__.py with all the get_version stuff.
diff --git a/django/bin/daily_cleanup.py b/django/bin/daily_cleanup.py
index c9f4cb905c..ac3de00f2c 100755
--- a/django/bin/daily_cleanup.py
+++ b/django/bin/daily_cleanup.py
@@ -7,7 +7,13 @@ Can be run as a cronjob to clean out old data from the database (only expired
sessions at the moment).
"""
+import warnings
+
from django.core import management
if __name__ == "__main__":
- management.call_command('cleanup')
+ warnings.warn(
+ "The `daily_cleanup` script has been deprecated "
+ "in favor of `django-admin.py clearsessions`.",
+ PendingDeprecationWarning)
+ management.call_command('clearsessions')
diff --git a/django/conf/__init__.py b/django/conf/__init__.py
index 1804c851bf..b00c8d5046 100644
--- a/django/conf/__init__.py
+++ b/django/conf/__init__.py
@@ -6,6 +6,7 @@ variable, and then from django.conf.global_settings; see the global settings fil
a list of all possible variables.
"""
+import logging
import os
import time # Needed for Windows
import warnings
@@ -55,6 +56,15 @@ class LazySettings(LazyObject):
"""
Setup logging from LOGGING_CONFIG and LOGGING settings.
"""
+ try:
+ # Route warnings through python logging
+ logging.captureWarnings(True)
+ # Allow DeprecationWarnings through the warnings filters
+ warnings.simplefilter("default", DeprecationWarning)
+ except AttributeError:
+ # No captureWarnings on Python 2.6, DeprecationWarnings are on anyway
+ pass
+
if self.LOGGING_CONFIG:
from django.utils.log import DEFAULT_LOGGING
# First find the logging configuration function ...
@@ -83,6 +93,7 @@ class LazySettings(LazyObject):
for name, value in options.items():
setattr(holder, name, value)
self._wrapped = holder
+ self._configure_logging()
@property
def configured(self):
@@ -99,9 +110,6 @@ class BaseSettings(object):
def __setattr__(self, name, value):
if name in ("MEDIA_URL", "STATIC_URL") and value and not value.endswith('/'):
raise ImproperlyConfigured("If set, %s must end with a slash" % name)
- elif name == "ADMIN_MEDIA_PREFIX":
- warnings.warn("The ADMIN_MEDIA_PREFIX setting has been removed; "
- "use STATIC_URL instead.", DeprecationWarning)
elif name == "ALLOWED_INCLUDE_ROOTS" and isinstance(value, six.string_types):
raise ValueError("The ALLOWED_INCLUDE_ROOTS setting must be set "
"to a tuple, not a string.")
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index f1cbb22880..c533efc41c 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -150,12 +150,8 @@ SERVER_EMAIL = 'root@localhost'
# Whether to send broken-link emails.
SEND_BROKEN_LINK_EMAILS = False
-# Database connection info.
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.dummy',
- },
-}
+# Database connection info. If left empty, will default to the dummy backend.
+DATABASES = {}
# Classes used to implement DB routing behavior.
DATABASE_ROUTERS = []
@@ -449,6 +445,7 @@ MIDDLEWARE_CLASSES = (
# SESSIONS #
############
+SESSION_CACHE_ALIAS = 'default' # Cache to store session data if using the cache session backend.
SESSION_COOKIE_NAME = 'sessionid' # Cookie name. This can be whatever you want.
SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 # Age of cookie, in seconds (default: 2 weeks).
SESSION_COOKIE_DOMAIN = None # A string like ".example.com", or None for standard domain cookie.
diff --git a/django/conf/locale/__init__.py b/django/conf/locale/__init__.py
index 93e98194a4..45e56b6d19 100644
--- a/django/conf/locale/__init__.py
+++ b/django/conf/locale/__init__.py
@@ -1,5 +1,8 @@
from __future__ import unicode_literals
+# About name_local: capitalize it as if your language name was appearing
+# inside a sentence in your language.
+
LANG_INFO = {
'ar': {
'bidi': True,
@@ -53,7 +56,7 @@ LANG_INFO = {
'bidi': False,
'code': 'da',
'name': 'Danish',
- 'name_local': 'Dansk',
+ 'name_local': 'dansk',
},
'de': {
'bidi': False,
@@ -137,7 +140,7 @@ LANG_INFO = {
'bidi': False,
'code': 'fr',
'name': 'French',
- 'name_local': 'Fran\xe7ais',
+ 'name_local': 'fran\xe7ais',
},
'fy-nl': {
'bidi': False,
@@ -269,7 +272,7 @@ LANG_INFO = {
'bidi': False,
'code': 'nb',
'name': 'Norwegian Bokmal',
- 'name_local': 'Norsk (bokm\xe5l)',
+ 'name_local': 'norsk (bokm\xe5l)',
},
'ne': {
'bidi': False,
@@ -287,13 +290,13 @@ LANG_INFO = {
'bidi': False,
'code': 'nn',
'name': 'Norwegian Nynorsk',
- 'name_local': 'Norsk (nynorsk)',
+ 'name_local': 'norsk (nynorsk)',
},
'no': {
'bidi': False,
'code': 'no',
'name': 'Norwegian',
- 'name_local': 'Norsk',
+ 'name_local': 'norsk',
},
'pa': {
'bidi': False,
@@ -365,7 +368,7 @@ LANG_INFO = {
'bidi': False,
'code': 'sv',
'name': 'Swedish',
- 'name_local': 'Svenska',
+ 'name_local': 'svenska',
},
'sw': {
'bidi': False,
diff --git a/django/conf/locale/ca/formats.py b/django/conf/locale/ca/formats.py
index 9a0054d0fb..b9431b5b67 100644
--- a/django/conf/locale/ca/formats.py
+++ b/django/conf/locale/ca/formats.py
@@ -19,10 +19,6 @@ DATE_INPUT_FORMATS = (
# '31/12/2009', '31/12/09'
'%d/%m/%Y', '%d/%m/%y'
)
-TIME_INPUT_FORMATS = (
- # '14:30:59', '14:30'
- '%H:%M:%S', '%H:%M'
-)
DATETIME_INPUT_FORMATS = (
'%d/%m/%Y %H:%M:%S',
'%d/%m/%Y %H:%M',
diff --git a/django/conf/locale/cs/formats.py b/django/conf/locale/cs/formats.py
index f0b674227b..ea28718f57 100644
--- a/django/conf/locale/cs/formats.py
+++ b/django/conf/locale/cs/formats.py
@@ -17,21 +17,26 @@ FIRST_DAY_OF_WEEK = 1 # Monday
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
DATE_INPUT_FORMATS = (
- '%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
- '%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
+ '%d.%m.%Y', '%d.%m.%y', # '05.01.2006', '05.01.06'
+ '%d. %m. %Y', '%d. %m. %y', # '5. 1. 2006', '5. 1. 06'
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
)
+# Kept ISO formats as one is in first position
TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
+ '%H:%M:%S', # '04:30:59'
+ '%H.%M', # '04.30'
+ '%H:%M', # '04:30'
)
DATETIME_INPUT_FORMATS = (
- '%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
- '%d.%m.%Y %H:%M', # '25.10.2006 14:30'
- '%d.%m.%Y', # '25.10.2006'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
+ '%d.%m.%Y %H:%M:%S', # '05.01.2006 04:30:59'
+ '%d.%m.%Y %H.%M', # '05.01.2006 04.30'
+ '%d.%m.%Y %H:%M', # '05.01.2006 04:30'
+ '%d.%m.%Y', # '05.01.2006'
+ '%d. %m. %Y %H:%M:%S', # '05. 01. 2006 04:30:59'
+ '%d. %m. %Y %H.%M', # '05. 01. 2006 04.30'
+ '%d. %m. %Y %H:%M', # '05. 01. 2006 04:30'
+ '%d. %m. %Y', # '05. 01. 2006'
+ '%Y-%m-%d %H.%M', # '2006-01-05 04.30'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '\xa0' # non-breaking space
diff --git a/django/conf/locale/da/formats.py b/django/conf/locale/da/formats.py
index f6cdea8b59..9022eb2ed4 100644
--- a/django/conf/locale/da/formats.py
+++ b/django/conf/locale/da/formats.py
@@ -18,10 +18,6 @@ FIRST_DAY_OF_WEEK = 1
DATE_INPUT_FORMATS = (
'%d.%m.%Y', # '25.10.2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
diff --git a/django/conf/locale/de/formats.py b/django/conf/locale/de/formats.py
index 79d555b989..a75b806acb 100644
--- a/django/conf/locale/de/formats.py
+++ b/django/conf/locale/de/formats.py
@@ -17,20 +17,12 @@ FIRST_DAY_OF_WEEK = 1 # Monday
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
DATE_INPUT_FORMATS = (
'%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
- '%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
'%d.%m.%Y', # '25.10.2006'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '.'
diff --git a/django/conf/locale/de_CH/formats.py b/django/conf/locale/de_CH/formats.py
index 7cbf76e7db..667d1eced0 100644
--- a/django/conf/locale/de_CH/formats.py
+++ b/django/conf/locale/de_CH/formats.py
@@ -19,20 +19,12 @@ FIRST_DAY_OF_WEEK = 1 # Monday
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
DATE_INPUT_FORMATS = (
'%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
- '%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
'%d.%m.%Y', # '25.10.2006'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
# these are the separators for non-monetary numbers. For monetary numbers,
diff --git a/django/conf/locale/en/formats.py b/django/conf/locale/en/formats.py
index 6cf2335f5e..6bd693e60e 100644
--- a/django/conf/locale/en/formats.py
+++ b/django/conf/locale/en/formats.py
@@ -15,6 +15,7 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
# '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
@@ -22,10 +23,6 @@ DATE_INPUT_FORMATS = (
# '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006'
# '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
diff --git a/django/conf/locale/en_GB/formats.py b/django/conf/locale/en_GB/formats.py
index ea9a3f6303..b594aafb74 100644
--- a/django/conf/locale/en_GB/formats.py
+++ b/django/conf/locale/en_GB/formats.py
@@ -17,16 +17,11 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
DATE_INPUT_FORMATS = (
'%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
- '%Y-%m-%d', # '2006-10-25'
# '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
# '%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006'
# '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006'
# '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
diff --git a/django/conf/locale/es/formats.py b/django/conf/locale/es/formats.py
index f10351c649..6241158338 100644
--- a/django/conf/locale/es/formats.py
+++ b/django/conf/locale/es/formats.py
@@ -19,10 +19,6 @@ DATE_INPUT_FORMATS = (
# '31/12/2009', '31/12/09'
'%d/%m/%Y', '%d/%m/%y'
)
-TIME_INPUT_FORMATS = (
- # '14:30:59', '14:30'
- '%H:%M:%S', '%H:%M'
-)
DATETIME_INPUT_FORMATS = (
'%d/%m/%Y %H:%M:%S',
'%d/%m/%Y %H:%M',
diff --git a/django/conf/locale/es_AR/formats.py b/django/conf/locale/es_AR/formats.py
index e28841f90a..651690bfdf 100644
--- a/django/conf/locale/es_AR/formats.py
+++ b/django/conf/locale/es_AR/formats.py
@@ -19,10 +19,6 @@ DATE_INPUT_FORMATS = (
'%d/%m/%Y', # '31/12/2009'
'%d/%m/%y', # '31/12/09'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d/%m/%Y %H:%M:%S',
'%d/%m/%Y %H:%M',
diff --git a/django/conf/locale/es_MX/formats.py b/django/conf/locale/es_MX/formats.py
index cef6b4e2f9..e0e6d7f16d 100644
--- a/django/conf/locale/es_MX/formats.py
+++ b/django/conf/locale/es_MX/formats.py
@@ -15,9 +15,6 @@ DATE_INPUT_FORMATS = (
'%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
'%Y%m%d', # '20061025'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', '%H:%M', # '14:30:59', '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d/%m/%Y %H:%M:%S',
'%d/%m/%Y %H:%M',
diff --git a/django/conf/locale/es_NI/formats.py b/django/conf/locale/es_NI/formats.py
index fd5cc9a925..a22432a16a 100644
--- a/django/conf/locale/es_NI/formats.py
+++ b/django/conf/locale/es_NI/formats.py
@@ -15,9 +15,6 @@ DATE_INPUT_FORMATS = (
'%Y%m%d', # '20061025'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', '%H:%M', # '14:30:59', '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d/%m/%Y %H:%M:%S',
'%d/%m/%Y %H:%M',
diff --git a/django/conf/locale/fr/formats.py b/django/conf/locale/fr/formats.py
index 3b5e8861d8..1ef7ecb242 100644
--- a/django/conf/locale/fr/formats.py
+++ b/django/conf/locale/fr/formats.py
@@ -19,13 +19,8 @@ FIRST_DAY_OF_WEEK = 1 # Monday
DATE_INPUT_FORMATS = (
'%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
'%d.%m.%Y', '%d.%m.%y', # Swiss (fr_CH), '25.10.2006', '25.10.06'
- '%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
# '%d %B %Y', '%d %b %Y', # '25 octobre 2006', '25 oct. 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d/%m/%Y %H:%M:%S', # '25/10/2006 14:30:59'
'%d/%m/%Y %H:%M', # '25/10/2006 14:30'
@@ -33,9 +28,6 @@ DATETIME_INPUT_FORMATS = (
'%d.%m.%Y %H:%M:%S', # Swiss (fr_CH), '25.10.2006 14:30:59'
'%d.%m.%Y %H:%M', # Swiss (fr_CH), '25.10.2006 14:30'
'%d.%m.%Y', # Swiss (fr_CH), '25.10.2006'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '\xa0' # non-breaking space
diff --git a/django/conf/locale/hr/formats.py b/django/conf/locale/hr/formats.py
index 72fcf51fc7..9f4c74051e 100644
--- a/django/conf/locale/hr/formats.py
+++ b/django/conf/locale/hr/formats.py
@@ -15,15 +15,12 @@ FIRST_DAY_OF_WEEK = 1
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', # '2006-10-25'
'%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.'
'%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
diff --git a/django/conf/locale/it/formats.py b/django/conf/locale/it/formats.py
index 330ef4e150..de81fa6cdf 100644
--- a/django/conf/locale/it/formats.py
+++ b/django/conf/locale/it/formats.py
@@ -20,10 +20,6 @@ DATE_INPUT_FORMATS = (
'%d-%m-%Y', '%Y-%m-%d', # '25-10-2006', '2008-10-25'
'%d-%m-%y', '%d/%m/%y', # '25-10-06', '25/10/06'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d/%m/%Y %H:%M:%S', # '25/10/2006 14:30:59'
'%d/%m/%Y %H:%M', # '25/10/2006 14:30'
diff --git a/django/conf/locale/ka/formats.py b/django/conf/locale/ka/formats.py
index 5117e4aa02..c3552e0661 100644
--- a/django/conf/locale/ka/formats.py
+++ b/django/conf/locale/ka/formats.py
@@ -15,16 +15,13 @@ FIRST_DAY_OF_WEEK = 1 # (Monday)
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
# '%d %b %Y', '%d %b, %Y', '%d %b. %Y', # '25 Oct 2006', '25 Oct, 2006', '25 Oct. 2006'
# '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006'
# '%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
diff --git a/django/conf/locale/ko/formats.py b/django/conf/locale/ko/formats.py
index 3c0431e4bf..b794afe900 100644
--- a/django/conf/locale/ko/formats.py
+++ b/django/conf/locale/ko/formats.py
@@ -16,6 +16,7 @@ SHORT_DATETIME_FORMAT = 'Y-n-j H:i'
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
# '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
diff --git a/django/conf/locale/lv/formats.py b/django/conf/locale/lv/formats.py
index e4ef524adf..f115a6e487 100644
--- a/django/conf/locale/lv/formats.py
+++ b/django/conf/locale/lv/formats.py
@@ -16,6 +16,7 @@ FIRST_DAY_OF_WEEK = 1 #Monday
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', '%d.%m.%Y', '%d.%m.%y', # '2006-10-25', '25.10.2006', '25.10.06'
)
diff --git a/django/conf/locale/mk/formats.py b/django/conf/locale/mk/formats.py
index e217e655e6..44feb512bf 100644
--- a/django/conf/locale/mk/formats.py
+++ b/django/conf/locale/mk/formats.py
@@ -18,12 +18,6 @@ FIRST_DAY_OF_WEEK = 1
DATE_INPUT_FORMATS = (
'%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.'
'%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.'
- '%Y-%m-%d', # '2006-10-25'
-)
-
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
)
DATETIME_INPUT_FORMATS = (
@@ -39,9 +33,6 @@ DATETIME_INPUT_FORMATS = (
'%d. %m. %y. %H:%M:%S', # '25. 10. 06. 14:30:59'
'%d. %m. %y. %H:%M', # '25. 10. 06. 14:30'
'%d. %m. %y.', # '25. 10. 06.'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
diff --git a/django/conf/locale/ml/formats.py b/django/conf/locale/ml/formats.py
index 6cf2335f5e..6bd693e60e 100644
--- a/django/conf/locale/ml/formats.py
+++ b/django/conf/locale/ml/formats.py
@@ -15,6 +15,7 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
# '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006'
@@ -22,10 +23,6 @@ DATE_INPUT_FORMATS = (
# '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006'
# '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
diff --git a/django/conf/locale/nb/formats.py b/django/conf/locale/nb/formats.py
index 4a896dd80d..9a009dc03a 100644
--- a/django/conf/locale/nb/formats.py
+++ b/django/conf/locale/nb/formats.py
@@ -16,22 +16,17 @@ FIRST_DAY_OF_WEEK = 1 # Monday
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', '%d.%m.%Y', '%d.%m.%y', # '2006-10-25', '25.10.2006', '25.10.06'
- '%Y-%m-%d', # '2006-10-25',
# '%d. %b %Y', '%d %b %Y', # '25. okt 2006', '25 okt 2006'
# '%d. %b. %Y', '%d %b. %Y', # '25. okt. 2006', '25 okt. 2006'
# '%d. %B %Y', '%d %B %Y', # '25. oktober 2006', '25 oktober 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
'%Y-%m-%d', # '2006-10-25'
- '%Y-%m-%d', # '2006-10-25'
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
'%d.%m.%Y', # '25.10.2006'
diff --git a/django/conf/locale/nl/formats.py b/django/conf/locale/nl/formats.py
index 758737e8c9..be5a146104 100644
--- a/django/conf/locale/nl/formats.py
+++ b/django/conf/locale/nl/formats.py
@@ -16,10 +16,11 @@ FIRST_DAY_OF_WEEK = 1 # Monday (in Dutch 'maandag')
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
DATE_INPUT_FORMATS = (
- '%d-%m-%Y', '%d-%m-%y', '%Y-%m-%d', # '20-01-2009', '20-01-09', '2009-01-20'
- # '%d %b %Y', '%d %b %y', # '20 jan 2009', '20 jan 09'
- # '%d %B %Y', '%d %B %y', # '20 januari 2009', '20 januari 09'
+ '%d-%m-%Y', '%d-%m-%y', # '20-01-2009', '20-01-09'
+ # '%d %b %Y', '%d %b %y', # '20 jan 2009', '20 jan 09'
+ # '%d %B %Y', '%d %B %y', # '20 januari 2009', '20 januari 09'
)
+# Kept ISO formats as one is in first position
TIME_INPUT_FORMATS = (
'%H:%M:%S', # '15:23:35'
'%H.%M:%S', # '15.23:35'
diff --git a/django/conf/locale/nn/formats.py b/django/conf/locale/nn/formats.py
index 4a896dd80d..a9b8b562fc 100644
--- a/django/conf/locale/nn/formats.py
+++ b/django/conf/locale/nn/formats.py
@@ -16,17 +16,13 @@ FIRST_DAY_OF_WEEK = 1 # Monday
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', '%d.%m.%Y', '%d.%m.%y', # '2006-10-25', '25.10.2006', '25.10.06'
- '%Y-%m-%d', # '2006-10-25',
# '%d. %b %Y', '%d %b %Y', # '25. okt 2006', '25 okt 2006'
# '%d. %b. %Y', '%d %b. %Y', # '25. okt. 2006', '25 okt. 2006'
# '%d. %B %Y', '%d %B %Y', # '25. oktober 2006', '25 oktober 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
diff --git a/django/conf/locale/pl/formats.py b/django/conf/locale/pl/formats.py
index 021063d474..064e75f1ee 100644
--- a/django/conf/locale/pl/formats.py
+++ b/django/conf/locale/pl/formats.py
@@ -18,20 +18,13 @@ FIRST_DAY_OF_WEEK = 1 # Monday
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
DATE_INPUT_FORMATS = (
'%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
- '%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
+ '%y-%m-%d', # '06-10-25'
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
'%d.%m.%Y', # '25.10.2006'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = ' '
diff --git a/django/conf/locale/pt/formats.py b/django/conf/locale/pt/formats.py
index a6ce2c08de..2d6ca69647 100644
--- a/django/conf/locale/pt/formats.py
+++ b/django/conf/locale/pt/formats.py
@@ -15,15 +15,12 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', '%d/%m/%Y', '%d/%m/%y', # '2006-10-25', '25/10/2006', '25/10/06'
# '%d de %b de %Y', '%d de %b, %Y', # '25 de Out de 2006', '25 Out, 2006'
# '%d de %B de %Y', '%d de %B, %Y', # '25 de Outubro de 2006', '25 de Outubro, 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
diff --git a/django/conf/locale/pt_BR/formats.py b/django/conf/locale/pt_BR/formats.py
index 6d9edeecce..7a6e0877c3 100644
--- a/django/conf/locale/pt_BR/formats.py
+++ b/django/conf/locale/pt_BR/formats.py
@@ -17,14 +17,10 @@ FIRST_DAY_OF_WEEK = 0 # Sunday
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
DATE_INPUT_FORMATS = (
- '%d/%m/%Y', '%d/%m/%y', '%Y-%m-%d', # '25/10/2006', '25/10/06', '2006-10-25'
+ '%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
# '%d de %b de %Y', '%d de %b, %Y', # '25 de Out de 2006', '25 Out, 2006'
# '%d de %B de %Y', '%d de %B, %Y', # '25 de Outubro de 2006', '25 de Outubro, 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d/%m/%Y %H:%M:%S', # '25/10/2006 14:30:59'
'%d/%m/%Y %H:%M', # '25/10/2006 14:30'
@@ -32,9 +28,6 @@ DATETIME_INPUT_FORMATS = (
'%d/%m/%y %H:%M:%S', # '25/10/06 14:30:59'
'%d/%m/%y %H:%M', # '25/10/06 14:30'
'%d/%m/%y', # '25/10/06'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '.'
diff --git a/django/conf/locale/ru/formats.py b/django/conf/locale/ru/formats.py
index ec46bff400..d692e67833 100644
--- a/django/conf/locale/ru/formats.py
+++ b/django/conf/locale/ru/formats.py
@@ -19,12 +19,7 @@ FIRST_DAY_OF_WEEK = 1 # Monday
DATE_INPUT_FORMATS = (
'%d.%m.%Y', # '25.10.2006'
'%d.%m.%y', # '25.10.06'
- '%Y-%m-%d', # '2006-10-25'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
@@ -32,9 +27,6 @@ DATETIME_INPUT_FORMATS = (
'%d.%m.%y %H:%M:%S', # '25.10.06 14:30:59'
'%d.%m.%y %H:%M', # '25.10.06 14:30'
'%d.%m.%y', # '25.10.06'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '\xa0' # non-breaking space
diff --git a/django/conf/locale/sk/formats.py b/django/conf/locale/sk/formats.py
index 4b2201f49a..85e2cd8af9 100644
--- a/django/conf/locale/sk/formats.py
+++ b/django/conf/locale/sk/formats.py
@@ -18,20 +18,13 @@ FIRST_DAY_OF_WEEK = 1 # Monday
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
DATE_INPUT_FORMATS = (
'%d.%m.%Y', '%d.%m.%y', # '25.10.2006', '25.10.06'
- '%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
+ '%y-%m-%d', # '06-10-25'
# '%d. %B %Y', '%d. %b. %Y', # '25. October 2006', '25. Oct. 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
'%d.%m.%Y', # '25.10.2006'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '\xa0' # non-breaking space
diff --git a/django/conf/locale/sl/formats.py b/django/conf/locale/sl/formats.py
index f019323bcd..0d6137e1ed 100644
--- a/django/conf/locale/sl/formats.py
+++ b/django/conf/locale/sl/formats.py
@@ -21,11 +21,6 @@ DATE_INPUT_FORMATS = (
'%d. %m. %Y', '%d. %m. %y', # '25. 10. 2006', '25. 10. 06'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
-
DATETIME_INPUT_FORMATS = (
'%d.%m.%Y %H:%M:%S', # '25.10.2006 14:30:59'
'%d.%m.%Y %H:%M', # '25.10.2006 14:30'
diff --git a/django/conf/locale/sr/formats.py b/django/conf/locale/sr/formats.py
index 5e05832f7b..227f20d723 100644
--- a/django/conf/locale/sr/formats.py
+++ b/django/conf/locale/sr/formats.py
@@ -18,15 +18,10 @@ FIRST_DAY_OF_WEEK = 1
DATE_INPUT_FORMATS = (
'%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.'
'%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.'
- '%Y-%m-%d', # '2006-10-25'
# '%d. %b %y.', '%d. %B %y.', # '25. Oct 06.', '25. October 06.'
# '%d. %b \'%y.', '%d. %B \'%y.', # '25. Oct '06.', '25. October '06.'
# '%d. %b %Y.', '%d. %B %Y.', # '25. Oct 2006.', '25. October 2006.'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d.%m.%Y. %H:%M:%S', # '25.10.2006. 14:30:59'
'%d.%m.%Y. %H:%M', # '25.10.2006. 14:30'
@@ -40,9 +35,6 @@ DATETIME_INPUT_FORMATS = (
'%d. %m. %y. %H:%M:%S', # '25. 10. 06. 14:30:59'
'%d. %m. %y. %H:%M', # '25. 10. 06. 14:30'
'%d. %m. %y.', # '25. 10. 06.'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '.'
diff --git a/django/conf/locale/sr_Latn/formats.py b/django/conf/locale/sr_Latn/formats.py
index 5e05832f7b..227f20d723 100644
--- a/django/conf/locale/sr_Latn/formats.py
+++ b/django/conf/locale/sr_Latn/formats.py
@@ -18,15 +18,10 @@ FIRST_DAY_OF_WEEK = 1
DATE_INPUT_FORMATS = (
'%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.'
'%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.'
- '%Y-%m-%d', # '2006-10-25'
# '%d. %b %y.', '%d. %B %y.', # '25. Oct 06.', '25. October 06.'
# '%d. %b \'%y.', '%d. %B \'%y.', # '25. Oct '06.', '25. October '06.'
# '%d. %b %Y.', '%d. %B %Y.', # '25. Oct 2006.', '25. October 2006.'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d.%m.%Y. %H:%M:%S', # '25.10.2006. 14:30:59'
'%d.%m.%Y. %H:%M', # '25.10.2006. 14:30'
@@ -40,9 +35,6 @@ DATETIME_INPUT_FORMATS = (
'%d. %m. %y. %H:%M:%S', # '25. 10. 06. 14:30:59'
'%d. %m. %y. %H:%M', # '25. 10. 06. 14:30'
'%d. %m. %y.', # '25. 10. 06.'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '.'
diff --git a/django/conf/locale/sv/formats.py b/django/conf/locale/sv/formats.py
index 767dbe8d3e..0f52c2c1a5 100644
--- a/django/conf/locale/sv/formats.py
+++ b/django/conf/locale/sv/formats.py
@@ -16,15 +16,12 @@ FIRST_DAY_OF_WEEK = 1
# The *_INPUT_FORMATS strings use the Python strftime format syntax,
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
+# Kept ISO formats as they are in first position
DATE_INPUT_FORMATS = (
'%Y-%m-%d', # '2006-10-25'
'%m/%d/%Y', # '10/25/2006'
'%m/%d/%y', # '10/25/06'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
'%Y-%m-%d %H:%M', # '2006-10-25 14:30'
diff --git a/django/conf/locale/tr/formats.py b/django/conf/locale/tr/formats.py
index 5fb2e42f09..705b2ed659 100644
--- a/django/conf/locale/tr/formats.py
+++ b/django/conf/locale/tr/formats.py
@@ -17,20 +17,13 @@ FIRST_DAY_OF_WEEK = 1 # Pazartesi
# see http://docs.python.org/library/datetime.html#strftime-strptime-behavior
DATE_INPUT_FORMATS = (
'%d/%m/%Y', '%d/%m/%y', # '25/10/2006', '25/10/06'
- '%Y-%m-%d', '%y-%m-%d', # '2006-10-25', '06-10-25'
+ '%y-%m-%d', # '06-10-25'
# '%d %B %Y', '%d %b. %Y', # '25 Ekim 2006', '25 Eki. 2006'
)
-TIME_INPUT_FORMATS = (
- '%H:%M:%S', # '14:30:59'
- '%H:%M', # '14:30'
-)
DATETIME_INPUT_FORMATS = (
'%d/%m/%Y %H:%M:%S', # '25/10/2006 14:30:59'
'%d/%m/%Y %H:%M', # '25/10/2006 14:30'
'%d/%m/%Y', # '25/10/2006'
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
)
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '.'
diff --git a/django/conf/project_template/project_name/settings.py b/django/conf/project_template/project_name/settings.py
index 6bdaa34988..559e27ca16 100644
--- a/django/conf/project_template/project_name/settings.py
+++ b/django/conf/project_template/project_name/settings.py
@@ -75,7 +75,7 @@ STATICFILES_DIRS = (
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
-# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
+ # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
@@ -85,7 +85,7 @@ SECRET_KEY = '{{ secret_key }}'
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
-# 'django.template.loaders.eggs.Loader',
+ # 'django.template.loaders.eggs.Loader',
)
MIDDLEWARE_CLASSES = (
diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py
index 0a34f807b3..ff66d3e3f3 100644
--- a/django/contrib/admin/filters.py
+++ b/django/contrib/admin/filters.py
@@ -9,7 +9,7 @@ import datetime
from django.db import models
from django.core.exceptions import ImproperlyConfigured, ValidationError
-from django.utils.encoding import smart_text
+from django.utils.encoding import smart_text, force_text
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.contrib.admin.util import (get_model_from_relation,
@@ -102,7 +102,7 @@ class SimpleListFilter(ListFilter):
}
for lookup, title in self.lookup_choices:
yield {
- 'selected': self.value() == lookup,
+ 'selected': self.value() == force_text(lookup),
'query_string': cl.get_query_string({
self.parameter_name: lookup,
}, []),
diff --git a/django/contrib/admin/forms.py b/django/contrib/admin/forms.py
index f1e7076ece..1fabdce245 100644
--- a/django/contrib/admin/forms.py
+++ b/django/contrib/admin/forms.py
@@ -6,8 +6,8 @@ from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm
from django.utils.translation import ugettext_lazy
-ERROR_MESSAGE = ugettext_lazy("Please enter the correct username and password "
- "for a staff account. Note that both fields are case-sensitive.")
+ERROR_MESSAGE = ugettext_lazy("Please enter the correct %(username)s and password "
+ "for a staff account. Note that both fields may be case-sensitive.")
class AdminAuthenticationForm(AuthenticationForm):
@@ -26,8 +26,12 @@ class AdminAuthenticationForm(AuthenticationForm):
if username and password:
self.user_cache = authenticate(username=username, password=password)
if self.user_cache is None:
- raise forms.ValidationError(message)
+ raise forms.ValidationError(message % {
+ 'username': self.username_field.verbose_name
+ })
elif not self.user_cache.is_active or not self.user_cache.is_staff:
- raise forms.ValidationError(message)
+ raise forms.ValidationError(message % {
+ 'username': self.username_field.verbose_name
+ })
self.check_for_test_cookie()
return self.cleaned_data
diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 90370bd978..4203287123 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -186,9 +186,7 @@ class AdminReadonlyField(object):
if getattr(attr, "allow_tags", False):
result_repr = mark_safe(result_repr)
else:
- if value is None:
- result_repr = EMPTY_CHANGELIST_VALUE
- elif isinstance(f.rel, ManyToManyRel):
+ if isinstance(f.rel, ManyToManyRel) and value is not None:
result_repr = ", ".join(map(six.text_type, value.all()))
else:
result_repr = display_for_field(value, f)
diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py
index e1d3b40d01..b697d7bdc8 100644
--- a/django/contrib/admin/models.py
+++ b/django/contrib/admin/models.py
@@ -4,7 +4,7 @@ from django.db import models
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.admin.util import quote
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext, ugettext_lazy as _
from django.utils.encoding import smart_text
from django.utils.encoding import python_2_unicode_compatible
@@ -42,13 +42,16 @@ class LogEntry(models.Model):
def __str__(self):
if self.action_flag == ADDITION:
- return _('Added "%(object)s".') % {'object': self.object_repr}
+ return ugettext('Added "%(object)s".') % {'object': self.object_repr}
elif self.action_flag == CHANGE:
- return _('Changed "%(object)s" - %(changes)s') % {'object': self.object_repr, 'changes': self.change_message}
+ return ugettext('Changed "%(object)s" - %(changes)s') % {
+ 'object': self.object_repr,
+ 'changes': self.change_message,
+ }
elif self.action_flag == DELETION:
- return _('Deleted "%(object)s."') % {'object': self.object_repr}
+ return ugettext('Deleted "%(object)s."') % {'object': self.object_repr}
- return _('LogEntry Object')
+ return ugettext('LogEntry Object')
def is_addition(self):
return self.action_flag == ADDITION
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 19c212db9a..1827d40159 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1,3 +1,4 @@
+import copy
from functools import update_wrapper, partial
import warnings
@@ -130,7 +131,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
# passed to formfield_for_dbfield override the defaults.
for klass in db_field.__class__.mro():
if klass in self.formfield_overrides:
- kwargs = dict(self.formfield_overrides[klass], **kwargs)
+ kwargs = dict(copy.deepcopy(self.formfield_overrides[klass]), **kwargs)
return db_field.formfield(**kwargs)
# For any other type of field, just call its formfield() method.
@@ -407,8 +408,6 @@ class ModelAdmin(BaseModelAdmin):
js.append('actions%s.js' % extra)
if self.prepopulated_fields:
js.extend(['urlify.js', 'prepopulate%s.js' % extra])
- if self.opts.get_ordered_objects():
- js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js'])
return forms.Media(js=[static('admin/js/%s' % url) for url in js])
def get_model_perms(self, request):
@@ -552,7 +551,7 @@ class ModelAdmin(BaseModelAdmin):
"""
from django.contrib.admin.models import LogEntry, DELETION
LogEntry.objects.log_action(
- user_id = request.user.id,
+ user_id = request.user.pk,
content_type_id = ContentType.objects.get_for_model(self.model).pk,
object_id = object.pk,
object_repr = object_repr,
@@ -665,6 +664,13 @@ class ModelAdmin(BaseModelAdmin):
# Use only the first item in list_display as link
return list(list_display)[:1]
+ def get_list_filter(self, request):
+ """
+ Returns a sequence containing the fields to be displayed as filters in
+ the right sidebar of the changelist page.
+ """
+ return self.list_filter
+
def construct_change_message(self, request, form, formsets):
"""
Construct a change message from a changed object.
@@ -691,12 +697,30 @@ class ModelAdmin(BaseModelAdmin):
change_message = ' '.join(change_message)
return change_message or _('No fields changed.')
- def message_user(self, request, message):
+ def message_user(self, request, message, level=messages.INFO, extra_tags='',
+ fail_silently=False):
"""
Send a message to the user. The default implementation
posts a message using the django.contrib.messages backend.
+
+ Exposes almost the same API as messages.add_message(), but accepts the
+ positional arguments in a different order to maintain backwards
+ compatibility. For convenience, it accepts the `level` argument as
+ a string rather than the usual level number.
"""
- messages.info(request, message)
+
+ if not isinstance(level, int):
+ # attempt to get the level if passed a string
+ try:
+ level = getattr(messages.constants, level.upper())
+ except AttributeError:
+ levels = messages.constants.DEFAULT_TAGS.values()
+ levels_repr = ', '.join('`%s`' % l for l in levels)
+ raise ValueError('Bad message level string: `%s`. '
+ 'Possible values are: %s' % (level, levels_repr))
+
+ messages.add_message(request, level, message, extra_tags=extra_tags,
+ fail_silently=fail_silently)
def save_form(self, request, form, change):
"""
@@ -738,7 +762,6 @@ class ModelAdmin(BaseModelAdmin):
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
opts = self.model._meta
app_label = opts.app_label
- ordered_objects = opts.get_ordered_objects()
context.update({
'add': add,
'change': change,
@@ -747,7 +770,6 @@ class ModelAdmin(BaseModelAdmin):
'has_delete_permission': self.has_delete_permission(request, obj),
'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
'has_absolute_url': hasattr(self.model, 'get_absolute_url'),
- 'ordered_objects': ordered_objects,
'form_url': form_url,
'opts': opts,
'content_type_id': ContentType.objects.get_for_model(self.model).id,
@@ -1174,6 +1196,7 @@ class ModelAdmin(BaseModelAdmin):
list_display = self.get_list_display(request)
list_display_links = self.get_list_display_links(request, list_display)
+ list_filter = self.get_list_filter(request)
# Check actions to see if any are available on this changelist
actions = self.get_actions(request)
@@ -1184,7 +1207,7 @@ class ModelAdmin(BaseModelAdmin):
ChangeList = self.get_changelist(request)
try:
cl = ChangeList(request, self.model, list_display,
- list_display_links, self.list_filter, self.date_hierarchy,
+ list_display_links, list_filter, self.date_hierarchy,
self.search_fields, self.list_select_related,
self.list_per_page, self.list_max_show_all, self.list_editable,
self)
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
index e375bc608f..185417015a 100644
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -354,6 +354,7 @@ class AdminSite(object):
info = (app_label, model._meta.module_name)
model_dict = {
'name': capfirst(model._meta.verbose_name_plural),
+ 'object_name': model._meta.object_name,
'perms': perms,
}
if perms.get('change', False):
@@ -371,6 +372,7 @@ class AdminSite(object):
else:
app_dict[app_label] = {
'name': app_label.title(),
+ 'app_label': app_label,
'app_url': reverse('admin:app_list', kwargs={'app_label': app_label}, current_app=self.name),
'has_module_perms': has_module_perms,
'models': [model_dict],
@@ -389,9 +391,9 @@ class AdminSite(object):
'app_list': app_list,
}
context.update(extra_context or {})
- return TemplateResponse(request, [
- self.index_template or 'admin/index.html',
- ], context, current_app=self.name)
+ return TemplateResponse(request, self.index_template or
+ 'admin/index.html', context,
+ current_app=self.name)
def app_index(self, request, app_label, extra_context=None):
user = request.user
@@ -408,6 +410,7 @@ class AdminSite(object):
info = (app_label, model._meta.module_name)
model_dict = {
'name': capfirst(model._meta.verbose_name_plural),
+ 'object_name': model._meta.object_name,
'perms': perms,
}
if perms.get('change', False):
@@ -428,6 +431,7 @@ class AdminSite(object):
# information.
app_dict = {
'name': app_label.title(),
+ 'app_label': app_label,
'app_url': '',
'has_module_perms': has_module_perms,
'models': [model_dict],
diff --git a/django/contrib/admin/static/admin/css/base.css b/django/contrib/admin/static/admin/css/base.css
index 5e5fc58a77..5ac4032c15 100644
--- a/django/contrib/admin/static/admin/css/base.css
+++ b/django/contrib/admin/static/admin/css/base.css
@@ -322,6 +322,10 @@ thead th.sorted {
background: #c5c5c5 url(../img/nav-bg-selected.gif) top left repeat-x;
}
+thead th.sorted .text {
+ padding-right: 42px;
+}
+
table thead th .text span {
padding: 2px 5px;
display:block;
diff --git a/django/contrib/admin/static/admin/css/rtl.css b/django/contrib/admin/static/admin/css/rtl.css
index 82d16024e6..ba9f1b5ad7 100644
--- a/django/contrib/admin/static/admin/css/rtl.css
+++ b/django/contrib/admin/static/admin/css/rtl.css
@@ -84,6 +84,11 @@ table thead th.sorted .sortoptions {
float: left;
}
+thead th.sorted .text {
+ padding-right: 0;
+ padding-left: 42px;
+}
+
/* dashboard styles */
.dashboard .module table td a {
diff --git a/django/contrib/admin/static/admin/css/widgets.css b/django/contrib/admin/static/admin/css/widgets.css
index 0a7012c7b2..3b19353e6f 100644
--- a/django/contrib/admin/static/admin/css/widgets.css
+++ b/django/contrib/admin/static/admin/css/widgets.css
@@ -225,6 +225,21 @@ table p.datetime {
padding-left: 0;
}
+/* URL */
+
+p.url {
+ line-height: 20px;
+ margin: 0;
+ padding: 0;
+ color: #666;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.url a {
+ font-weight: normal;
+}
+
/* FILE UPLOADS */
p.file-upload {
diff --git a/django/contrib/admin/static/admin/js/admin/ordering.js b/django/contrib/admin/static/admin/js/admin/ordering.js
deleted file mode 100644
index 595be4d62b..0000000000
--- a/django/contrib/admin/static/admin/js/admin/ordering.js
+++ /dev/null
@@ -1,137 +0,0 @@
-addEvent(window, 'load', reorder_init);
-
-var lis;
-var top = 0;
-var left = 0;
-var height = 30;
-
-function reorder_init() {
- lis = document.getElementsBySelector('ul#orderthese li');
- var input = document.getElementsBySelector('input[name=order_]')[0];
- setOrder(input.value.split(','));
- input.disabled = true;
- draw();
- // Now initialize the dragging behavior
- var limit = (lis.length - 1) * height;
- for (var i = 0; i < lis.length; i++) {
- var li = lis[i];
- var img = document.getElementById('handle'+li.id);
- li.style.zIndex = 1;
- Drag.init(img, li, left + 10, left + 10, top + 10, top + 10 + limit);
- li.onDragStart = startDrag;
- li.onDragEnd = endDrag;
- img.style.cursor = 'move';
- }
-}
-
-function submitOrderForm() {
- var inputOrder = document.getElementsBySelector('input[name=order_]')[0];
- inputOrder.value = getOrder();
- inputOrder.disabled=false;
-}
-
-function startDrag() {
- this.style.zIndex = '10';
- this.className = 'dragging';
-}
-
-function endDrag(x, y) {
- this.style.zIndex = '1';
- this.className = '';
- // Work out how far along it has been dropped, using x co-ordinate
- var oldIndex = this.index;
- var newIndex = Math.round((y - 10 - top) / height);
- // 'Snap' to the correct position
- this.style.top = (10 + top + newIndex * height) + 'px';
- this.index = newIndex;
- moveItem(oldIndex, newIndex);
-}
-
-function moveItem(oldIndex, newIndex) {
- // Swaps two items, adjusts the index and left co-ord for all others
- if (oldIndex == newIndex) {
- return; // Nothing to swap;
- }
- var direction, lo, hi;
- if (newIndex > oldIndex) {
- lo = oldIndex;
- hi = newIndex;
- direction = -1;
- } else {
- direction = 1;
- hi = oldIndex;
- lo = newIndex;
- }
- var lis2 = new Array(); // We will build the new order in this array
- for (var i = 0; i < lis.length; i++) {
- if (i < lo || i > hi) {
- // Position of items not between the indexes is unaffected
- lis2[i] = lis[i];
- continue;
- } else if (i == newIndex) {
- lis2[i] = lis[oldIndex];
- continue;
- } else {
- // Item is between the two indexes - move it along 1
- lis2[i] = lis[i - direction];
- }
- }
- // Re-index everything
- reIndex(lis2);
- lis = lis2;
- draw();
-// document.getElementById('hiddenOrder').value = getOrder();
- document.getElementsBySelector('input[name=order_]')[0].value = getOrder();
-}
-
-function reIndex(lis) {
- for (var i = 0; i < lis.length; i++) {
- lis[i].index = i;
- }
-}
-
-function draw() {
- for (var i = 0; i < lis.length; i++) {
- var li = lis[i];
- li.index = i;
- li.style.position = 'absolute';
- li.style.left = (10 + left) + 'px';
- li.style.top = (10 + top + (i * height)) + 'px';
- }
-}
-
-function getOrder() {
- var order = new Array(lis.length);
- for (var i = 0; i < lis.length; i++) {
- order[i] = lis[i].id.substring(1, 100);
- }
- return order.join(',');
-}
-
-function setOrder(id_list) {
- /* Set the current order to match the lsit of IDs */
- var temp_lis = new Array();
- for (var i = 0; i < id_list.length; i++) {
- var id = 'p' + id_list[i];
- temp_lis[temp_lis.length] = document.getElementById(id);
- }
- reIndex(temp_lis);
- lis = temp_lis;
- draw();
-}
-
-function addEvent(elm, evType, fn, useCapture)
-// addEvent and removeEvent
-// cross-browser event handling for IE5+, NS6 and Mozilla
-// By Scott Andrew
-{
- if (elm.addEventListener){
- elm.addEventListener(evType, fn, useCapture);
- return true;
- } else if (elm.attachEvent){
- var r = elm.attachEvent("on"+evType, fn);
- return r;
- } else {
- elm['on'+evType] = fn;
- }
-}
diff --git a/django/contrib/admin/static/admin/js/getElementsBySelector.js b/django/contrib/admin/static/admin/js/getElementsBySelector.js
deleted file mode 100644
index 15b57a1908..0000000000
--- a/django/contrib/admin/static/admin/js/getElementsBySelector.js
+++ /dev/null
@@ -1,167 +0,0 @@
-/* document.getElementsBySelector(selector)
- - returns an array of element objects from the current document
- matching the CSS selector. Selectors can contain element names,
- class names and ids and can be nested. For example:
-
- elements = document.getElementsBySelect('div#main p a.external')
-
- Will return an array of all 'a' elements with 'external' in their
- class attribute that are contained inside 'p' elements that are
- contained inside the 'div' element which has id="main"
-
- New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
- See http://www.w3.org/TR/css3-selectors/#attribute-selectors
-
- Version 0.4 - Simon Willison, March 25th 2003
- -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
- -- Opera 7 fails
-*/
-
-function getAllChildren(e) {
- // Returns all children of element. Workaround required for IE5/Windows. Ugh.
- return e.all ? e.all : e.getElementsByTagName('*');
-}
-
-document.getElementsBySelector = function(selector) {
- // Attempt to fail gracefully in lesser browsers
- if (!document.getElementsByTagName) {
- return new Array();
- }
- // Split selector in to tokens
- var tokens = selector.split(' ');
- var currentContext = new Array(document);
- for (var i = 0; i < tokens.length; i++) {
- token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');;
- if (token.indexOf('#') > -1) {
- // Token is an ID selector
- var bits = token.split('#');
- var tagName = bits[0];
- var id = bits[1];
- var element = document.getElementById(id);
- if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) {
- // ID not found or tag with that ID not found, return false.
- return new Array();
- }
- // Set currentContext to contain just this element
- currentContext = new Array(element);
- continue; // Skip to next token
- }
- if (token.indexOf('.') > -1) {
- // Token contains a class selector
- var bits = token.split('.');
- var tagName = bits[0];
- var className = bits[1];
- if (!tagName) {
- tagName = '*';
- }
- // Get elements matching tag, filter them for class selector
- var found = new Array;
- var foundCount = 0;
- for (var h = 0; h < currentContext.length; h++) {
- var elements;
- if (tagName == '*') {
- elements = getAllChildren(currentContext[h]);
- } else {
- try {
- elements = currentContext[h].getElementsByTagName(tagName);
- }
- catch(e) {
- elements = [];
- }
- }
- for (var j = 0; j < elements.length; j++) {
- found[foundCount++] = elements[j];
- }
- }
- currentContext = new Array;
- var currentContextIndex = 0;
- for (var k = 0; k < found.length; k++) {
- if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) {
- currentContext[currentContextIndex++] = found[k];
- }
- }
- continue; // Skip to next token
- }
- // Code to deal with attribute selectors
- if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
- var tagName = RegExp.$1;
- var attrName = RegExp.$2;
- var attrOperator = RegExp.$3;
- var attrValue = RegExp.$4;
- if (!tagName) {
- tagName = '*';
- }
- // Grab all of the tagName elements within current context
- var found = new Array;
- var foundCount = 0;
- for (var h = 0; h < currentContext.length; h++) {
- var elements;
- if (tagName == '*') {
- elements = getAllChildren(currentContext[h]);
- } else {
- elements = currentContext[h].getElementsByTagName(tagName);
- }
- for (var j = 0; j < elements.length; j++) {
- found[foundCount++] = elements[j];
- }
- }
- currentContext = new Array;
- var currentContextIndex = 0;
- var checkFunction; // This function will be used to filter the elements
- switch (attrOperator) {
- case '=': // Equality
- checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
- break;
- case '~': // Match one of space seperated words
- checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
- break;
- case '|': // Match start with value followed by optional hyphen
- checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
- break;
- case '^': // Match starts with value
- checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
- break;
- case '$': // Match ends with value - fails with "Warning" in Opera 7
- checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
- break;
- case '*': // Match ends with value
- checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
- break;
- default :
- // Just test for existence of attribute
- checkFunction = function(e) { return e.getAttribute(attrName); };
- }
- currentContext = new Array;
- var currentContextIndex = 0;
- for (var k = 0; k < found.length; k++) {
- if (checkFunction(found[k])) {
- currentContext[currentContextIndex++] = found[k];
- }
- }
- // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
- continue; // Skip to next token
- }
- // If we get here, token is JUST an element (not a class or ID selector)
- tagName = token;
- var found = new Array;
- var foundCount = 0;
- for (var h = 0; h < currentContext.length; h++) {
- var elements = currentContext[h].getElementsByTagName(tagName);
- for (var j = 0; j < elements.length; j++) {
- found[foundCount++] = elements[j];
- }
- }
- currentContext = found;
- }
- return currentContext;
-}
-
-/* That revolting regular expression explained
-/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
- \---/ \---/\-------------/ \-------/
- | | | |
- | | | The value
- | | ~,|,^,$,* or =
- | Attribute
- Tag
-*/
diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html
index b5a7715844..83a9c48ee1 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -1,5 +1,5 @@
{% extends "admin/base_site.html" %}
-{% load i18n admin_static admin_modify %}
+{% load i18n admin_static %}
{% load admin_urls %}
{% block extrahead %}{{ block.super }}
@@ -13,7 +13,7 @@
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
-&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}{{ original.pk }}">{{ original|truncatewords:"18" }}</a>
+&rsaquo; <a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a>
&rsaquo; {% trans 'Change password' %}
</div>
{% endblock %}
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index 4962e732a2..48846960b3 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -9,7 +9,7 @@
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />{% endblock %}
-{% block coltype %}{% if ordered_objects %}colMS{% else %}colM{% endif %}{% endblock %}
+{% block coltype %}colM{% endblock %}
{% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
diff --git a/django/contrib/admin/templates/admin/edit_inline/tabular.html b/django/contrib/admin/templates/admin/edit_inline/tabular.html
index f2757ede48..71a62ba71a 100644
--- a/django/contrib/admin/templates/admin/edit_inline/tabular.html
+++ b/django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -46,7 +46,7 @@
{% for field in line %}
<td{% if field.field.name %} class="field-{{ field.field.name }}"{% endif %}>
{% if field.is_readonly %}
- <p>{{ field.contents }}</p>
+ <p>{{ field.contents|linebreaksbr }}</p>
{% else %}
{{ field.field.errors.as_ul }}
{{ field.field }}
diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html
index c8d08c880f..09bc971d2f 100644
--- a/django/contrib/admin/templates/admin/includes/fieldset.html
+++ b/django/contrib/admin/templates/admin/includes/fieldset.html
@@ -14,7 +14,7 @@
{% else %}
{{ field.label_tag }}
{% if field.is_readonly %}
- <p>{{ field.contents }}</p>
+ <p>{{ field.contents|linebreaksbr }}</p>
{% else %}
{{ field.field }}
{% endif %}
diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
index 91ea0844b1..961e4823e0 100644
--- a/django/contrib/admin/templates/admin/index.html
+++ b/django/contrib/admin/templates/admin/index.html
@@ -14,7 +14,7 @@
{% if app_list %}
{% for app in app_list %}
- <div class="module">
+ <div class="app-{{ app.app_label }} module">
<table>
<caption>
<a href="{{ app.app_url }}" class="section" title="{% blocktrans with name=app.name %}Models in the {{ name }} application{% endblocktrans %}">
@@ -22,7 +22,7 @@
</a>
</caption>
{% for model in app.models %}
- <tr>
+ <tr class="model-{{ model.object_name|lower }}">
{% if model.admin_url %}
<th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
{% else %}
diff --git a/django/contrib/admin/templates/admin/submit_line.html b/django/contrib/admin/templates/admin/submit_line.html
index 8c9d22752d..38a97a1c6a 100644
--- a/django/contrib/admin/templates/admin/submit_line.html
+++ b/django/contrib/admin/templates/admin/submit_line.html
@@ -1,8 +1,8 @@
{% load i18n admin_urls %}
<div class="submit-row">
-{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" {{ onclick_attrib }}/>{% endif %}
+{% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save" />{% endif %}
{% if show_delete_link %}<p class="deletelink-box"><a href="{% url opts|admin_urlname:'delete' original.pk|admin_urlquote %}" class="deletelink">{% trans "Delete" %}</a></p>{% endif %}
-{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" {{ onclick_attrib }}/>{%endif%}
-{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" {{ onclick_attrib }}/>{% endif %}
-{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" {{ onclick_attrib }}/>{% endif %}
+{% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew" />{%endif%}
+{% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother" />{% endif %}
+{% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue" />{% endif %}
</div>
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py
index f6ac59635a..cecc6ed6c4 100644
--- a/django/contrib/admin/templatetags/admin_modify.py
+++ b/django/contrib/admin/templatetags/admin_modify.py
@@ -30,8 +30,6 @@ def submit_row(context):
save_as = context['save_as']
ctx = {
'opts': opts,
- 'onclick_attrib': (opts.get_ordered_objects() and change
- and 'onclick="submitOrderForm();"' or ''),
'show_delete_link': (not is_popup and context['has_delete_permission']
and change and context.get('show_delete', True)),
'show_save_as_new': not is_popup and change and save_as,
diff --git a/django/contrib/admin/templatetags/adminmedia.py b/django/contrib/admin/templatetags/adminmedia.py
deleted file mode 100644
index b08d13c18f..0000000000
--- a/django/contrib/admin/templatetags/adminmedia.py
+++ /dev/null
@@ -1,15 +0,0 @@
-import warnings
-from django.template import Library
-from django.templatetags.static import PrefixNode
-
-register = Library()
-
-@register.simple_tag
-def admin_media_prefix():
- """
- Returns the string contained in the setting ADMIN_MEDIA_PREFIX.
- """
- warnings.warn(
- "The admin_media_prefix template tag is deprecated. "
- "Use the static template tag instead.", DeprecationWarning)
- return PrefixNode.handle_simple("ADMIN_MEDIA_PREFIX")
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index 1e0bc2d366..1e6277fb87 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -10,7 +10,7 @@ from django.contrib.admin.templatetags.admin_static import static
from django.core.urlresolvers import reverse
from django.forms.widgets import RadioFieldRenderer
from django.forms.util import flatatt
-from django.utils.html import escape, format_html, format_html_join
+from django.utils.html import escape, format_html, format_html_join, smart_urlquote
from django.utils.text import Truncator
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
@@ -306,6 +306,19 @@ class AdminURLFieldWidget(forms.TextInput):
final_attrs.update(attrs)
super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)
+ def render(self, name, value, attrs=None):
+ html = super(AdminURLFieldWidget, self).render(name, value, attrs)
+ if value:
+ value = force_text(self._format_value(value))
+ final_attrs = {'href': mark_safe(smart_urlquote(value))}
+ html = format_html(
+ '<p class="url">{0} <a {1}>{2}</a><br />{3} {4}</p>',
+ _('Currently:'), flatatt(final_attrs), value,
+ _('Change:'), html
+ )
+ return html
+
+
class AdminIntegerFieldWidget(forms.TextInput):
class_name = 'vIntegerField'
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index 94963b4d39..cb0c116416 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -14,6 +14,7 @@ from django.core import urlresolvers
from django.contrib.admindocs import utils
from django.contrib.sites.models import Site
from django.utils.importlib import import_module
+from django.utils._os import upath
from django.utils import six
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
@@ -311,7 +312,7 @@ def load_all_installed_template_libraries():
try:
libraries = [
os.path.splitext(p)[0]
- for p in os.listdir(os.path.dirname(mod.__file__))
+ for p in os.listdir(os.path.dirname(upath(mod.__file__)))
if p.endswith('.py') and p[0].isalpha()
]
except OSError:
diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py
index dd4a8484f5..99348d3ae5 100644
--- a/django/contrib/auth/__init__.py
+++ b/django/contrib/auth/__init__.py
@@ -1,6 +1,6 @@
import re
-from django.core.exceptions import ImproperlyConfigured
+from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.utils.importlib import import_module
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
@@ -60,6 +60,9 @@ def authenticate(**credentials):
except TypeError:
# This backend doesn't accept these credentials as arguments. Try the next one.
continue
+ except PermissionDenied:
+ # This backend says to stop in our tracks - this user should not be allowed in at all.
+ return None
if user is None:
continue
# Annotate the user object with the path of the backend.
@@ -81,14 +84,14 @@ def login(request, user):
user = request.user
# TODO: It would be nice to support different login methods, like signed cookies.
if SESSION_KEY in request.session:
- if request.session[SESSION_KEY] != user.id:
+ if request.session[SESSION_KEY] != user.pk:
# To avoid reusing another user's session, create a new, empty
# session if the existing session corresponds to a different
# authenticated user.
request.session.flush()
else:
request.session.cycle_key()
- request.session[SESSION_KEY] = user.id
+ request.session[SESSION_KEY] = user.pk
request.session[BACKEND_SESSION_KEY] = user.backend
if hasattr(request, 'user'):
request.user = user
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
index 5f476f91c2..d15a387a7e 100644
--- a/django/contrib/auth/admin.py
+++ b/django/contrib/auth/admin.py
@@ -133,7 +133,7 @@ class UserAdmin(admin.ModelAdmin):
adminForm = admin.helpers.AdminForm(form, fieldsets, {})
context = {
- 'title': _('Change password: %s') % escape(user.username),
+ 'title': _('Change password: %s') % escape(user.get_username()),
'adminForm': adminForm,
'form_url': form_url,
'form': form,
@@ -148,10 +148,10 @@ class UserAdmin(admin.ModelAdmin):
'save_as': False,
'show_save': True,
}
- return TemplateResponse(request, [
+ return TemplateResponse(request,
self.change_user_password_template or
- 'admin/auth/user/change_password.html'
- ], context, current_app=self.admin_site.name)
+ 'admin/auth/user/change_password.html',
+ context, current_app=self.admin_site.name)
def response_add(self, request, obj, **kwargs):
"""
diff --git a/django/contrib/auth/context_processors.py b/django/contrib/auth/context_processors.py
index 5929505359..3d17fe2754 100644
--- a/django/contrib/auth/context_processors.py
+++ b/django/contrib/auth/context_processors.py
@@ -18,7 +18,9 @@ class PermLookupDict(object):
def __bool__(self):
return self.user.has_module_perms(self.module_name)
- __nonzero__ = __bool__ # Python 2
+
+ def __nonzero__(self): # Python 2 compatibility
+ return type(self).__bool__(self)
class PermWrapper(object):
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 423e3429e6..4e2f476cec 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -27,7 +27,7 @@ class ReadOnlyPasswordHashWidget(forms.Widget):
encoded = value
final_attrs = self.build_attrs(attrs)
- if encoded == '' or encoded == UNUSABLE_PASSWORD:
+ if not encoded or encoded == UNUSABLE_PASSWORD:
summary = mark_safe("<strong>%s</strong>" % ugettext("No password set."))
else:
try:
@@ -52,6 +52,11 @@ class ReadOnlyPasswordHashField(forms.Field):
kwargs.setdefault("required", False)
super(ReadOnlyPasswordHashField, self).__init__(*args, **kwargs)
+ def bound_data(self, data, initial):
+ # Always return initial because the widget doesn't
+ # render an input field.
+ return initial
+
class UserCreationForm(forms.ModelForm):
"""
@@ -143,8 +148,8 @@ class AuthenticationForm(forms.Form):
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
error_messages = {
- 'invalid_login': _("Please enter a correct username and password. "
- "Note that both fields are case-sensitive."),
+ 'invalid_login': _("Please enter a correct %(username)s and password. "
+ "Note that both fields may be case-sensitive."),
'no_cookies': _("Your Web browser doesn't appear to have cookies "
"enabled. Cookies are required for logging in."),
'inactive': _("This account is inactive."),
@@ -163,8 +168,8 @@ class AuthenticationForm(forms.Form):
# Set the label for the "username" field.
UserModel = get_user_model()
- username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
- self.fields['username'].label = capfirst(username_field.verbose_name)
+ self.username_field = UserModel._meta.get_field(UserModel.USERNAME_FIELD)
+ self.fields['username'].label = capfirst(self.username_field.verbose_name)
def clean(self):
username = self.cleaned_data.get('username')
@@ -175,7 +180,9 @@ class AuthenticationForm(forms.Form):
password=password)
if self.user_cache is None:
raise forms.ValidationError(
- self.error_messages['invalid_login'])
+ self.error_messages['invalid_login'] % {
+ 'username': self.username_field.verbose_name
+ })
elif not self.user_cache.is_active:
raise forms.ValidationError(self.error_messages['inactive'])
self.check_for_test_cookie()
@@ -209,10 +216,12 @@ class PasswordResetForm(forms.Form):
"""
UserModel = get_user_model()
email = self.cleaned_data["email"]
- self.users_cache = UserModel.objects.filter(email__iexact=email,
- is_active=True)
+ self.users_cache = UserModel.objects.filter(email__iexact=email)
if not len(self.users_cache):
raise forms.ValidationError(self.error_messages['unknown'])
+ if not any(user.is_active for user in self.users_cache):
+ # none of the filtered users are active
+ raise forms.ValidationError(self.error_messages['unknown'])
if any((user.password == UNUSABLE_PASSWORD)
for user in self.users_cache):
raise forms.ValidationError(self.error_messages['unusable'])
@@ -239,7 +248,7 @@ class PasswordResetForm(forms.Form):
'email': user.email,
'domain': domain,
'site_name': site_name,
- 'uid': int_to_base36(user.id),
+ 'uid': int_to_base36(user.pk),
'user': user,
'token': token_generator.make_token(user),
'protocol': use_https and 'https' or 'http',
diff --git a/django/contrib/auth/handlers/modwsgi.py b/django/contrib/auth/handlers/modwsgi.py
index 3229c6714b..5ee4d609f7 100644
--- a/django/contrib/auth/handlers/modwsgi.py
+++ b/django/contrib/auth/handlers/modwsgi.py
@@ -21,17 +21,12 @@ def check_password(environ, username, password):
user = UserModel.objects.get_by_natural_key(username)
except UserModel.DoesNotExist:
return None
- try:
- if not user.is_active:
- return None
- except AttributeError as e:
- # a custom user may not support is_active
+ if not user.is_active:
return None
return user.check_password(password)
finally:
db.close_connection()
-
def groups_for_user(environ, username):
"""
Authorizes a user based on groups
diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py
index b5fd29a1c2..ce5d57fa79 100644
--- a/django/contrib/auth/management/__init__.py
+++ b/django/contrib/auth/management/__init__.py
@@ -10,6 +10,7 @@ import unicodedata
from django.contrib.auth import models as auth_app, get_user_model
from django.core import exceptions
from django.core.management.base import CommandError
+from django.db import DEFAULT_DB_ALIAS, router
from django.db.models import get_models, signals
from django.utils import six
from django.utils.six.moves import input
@@ -57,7 +58,10 @@ def _check_permission_clashing(custom, builtin, ctype):
(codename, ctype.app_label, ctype.model_class().__name__))
pool.add(codename)
-def create_permissions(app, created_models, verbosity, **kwargs):
+def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kwargs):
+ if not router.allow_syncdb(db, auth_app.Permission):
+ return
+
from django.contrib.contenttypes.models import ContentType
app_models = get_models(app)
@@ -68,7 +72,9 @@ def create_permissions(app, created_models, verbosity, **kwargs):
# The codenames and ctypes that should exist.
ctypes = set()
for klass in app_models:
- ctype = ContentType.objects.get_for_model(klass)
+ # Force looking up the content types in the current database
+ # before creating foreign keys to them.
+ ctype = ContentType.objects.db_manager(db).get_for_model(klass)
ctypes.add(ctype)
for perm in _get_all_permissions(klass._meta, ctype):
searched_perms.append((ctype, perm))
@@ -76,21 +82,21 @@ def create_permissions(app, created_models, verbosity, **kwargs):
# Find all the Permissions that have a context_type for a model we're
# looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create.
- all_perms = set(auth_app.Permission.objects.filter(
+ all_perms = set(auth_app.Permission.objects.using(db).filter(
content_type__in=ctypes,
).values_list(
"content_type", "codename"
))
- objs = [
+ perms = [
auth_app.Permission(codename=codename, name=name, content_type=ctype)
for ctype, (codename, name) in searched_perms
if (ctype.pk, codename) not in all_perms
]
- auth_app.Permission.objects.bulk_create(objs)
+ auth_app.Permission.objects.using(db).bulk_create(perms)
if verbosity >= 2:
- for obj in objs:
- print("Adding permission '%s'" % obj)
+ for perm in perms:
+ print("Adding permission '%s'" % perm)
def create_superuser(app, created_models, verbosity, db, **kwargs):
diff --git a/django/contrib/auth/middleware.py b/django/contrib/auth/middleware.py
index 0398cfaf1e..f38efdd1d2 100644
--- a/django/contrib/auth/middleware.py
+++ b/django/contrib/auth/middleware.py
@@ -1,4 +1,6 @@
from django.contrib import auth
+from django.contrib.auth import load_backend
+from django.contrib.auth.backends import RemoteUserBackend
from django.core.exceptions import ImproperlyConfigured
from django.utils.functional import SimpleLazyObject
@@ -47,9 +49,18 @@ class RemoteUserMiddleware(object):
try:
username = request.META[self.header]
except KeyError:
- # If specified header doesn't exist then return (leaving
- # request.user set to AnonymousUser by the
- # AuthenticationMiddleware).
+ # If specified header doesn't exist then remove any existing
+ # authenticated remote-user, or return (leaving request.user set to
+ # AnonymousUser by the AuthenticationMiddleware).
+ if request.user.is_authenticated():
+ try:
+ stored_backend = load_backend(request.session.get(
+ auth.BACKEND_SESSION_KEY, ''))
+ if isinstance(stored_backend, RemoteUserBackend):
+ auth.logout(request)
+ except ImproperlyConfigured as e:
+ # backend failed to load
+ auth.logout(request)
return
# If the user is already authenticated and that user is the user we are
# getting passed in the headers, then the correct user is already
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index bd7bf4a162..7d62810923 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -195,43 +195,13 @@ class UserManager(BaseUserManager):
return u
-# A few helper functions for common logic between User and AnonymousUser.
-def _user_get_all_permissions(user, obj):
- permissions = set()
- for backend in auth.get_backends():
- if hasattr(backend, "get_all_permissions"):
- if obj is not None:
- permissions.update(backend.get_all_permissions(user, obj))
- else:
- permissions.update(backend.get_all_permissions(user))
- return permissions
-
-
-def _user_has_perm(user, perm, obj):
- for backend in auth.get_backends():
- if hasattr(backend, "has_perm"):
- if obj is not None:
- if backend.has_perm(user, perm, obj):
- return True
- else:
- if backend.has_perm(user, perm):
- return True
- return False
-
-
-def _user_has_module_perms(user, app_label):
- for backend in auth.get_backends():
- if hasattr(backend, "has_module_perms"):
- if backend.has_module_perms(user, app_label):
- return True
- return False
-
-
@python_2_unicode_compatible
class AbstractBaseUser(models.Model):
password = models.CharField(_('password'), max_length=128)
last_login = models.DateTimeField(_('last login'), default=timezone.now)
+ is_active = True
+
REQUIRED_FIELDS = []
class Meta:
@@ -288,32 +258,46 @@ class AbstractBaseUser(models.Model):
raise NotImplementedError()
-class AbstractUser(AbstractBaseUser):
- """
- An abstract base class implementing a fully featured User model with
- admin-compliant permissions.
+# A few helper functions for common logic between User and AnonymousUser.
+def _user_get_all_permissions(user, obj):
+ permissions = set()
+ for backend in auth.get_backends():
+ if hasattr(backend, "get_all_permissions"):
+ if obj is not None:
+ permissions.update(backend.get_all_permissions(user, obj))
+ else:
+ permissions.update(backend.get_all_permissions(user))
+ return permissions
- Username, password and email are required. Other fields are optional.
+
+def _user_has_perm(user, perm, obj):
+ for backend in auth.get_backends():
+ if hasattr(backend, "has_perm"):
+ if obj is not None:
+ if backend.has_perm(user, perm, obj):
+ return True
+ else:
+ if backend.has_perm(user, perm):
+ return True
+ return False
+
+
+def _user_has_module_perms(user, app_label):
+ for backend in auth.get_backends():
+ if hasattr(backend, "has_module_perms"):
+ if backend.has_module_perms(user, app_label):
+ return True
+ return False
+
+
+class PermissionsMixin(models.Model):
+ """
+ A mixin class that adds the fields and methods necessary to support
+ Django's Group and Permission model using the ModelBackend.
"""
- username = models.CharField(_('username'), max_length=30, unique=True,
- help_text=_('Required. 30 characters or fewer. Letters, numbers and '
- '@/./+/-/_ characters'),
- validators=[
- validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
- ])
- first_name = models.CharField(_('first name'), max_length=30, blank=True)
- last_name = models.CharField(_('last name'), max_length=30, blank=True)
- email = models.EmailField(_('email address'), blank=True)
- is_staff = models.BooleanField(_('staff status'), default=False,
- help_text=_('Designates whether the user can log into this admin '
- 'site.'))
- is_active = models.BooleanField(_('active'), default=True,
- help_text=_('Designates whether this user should be treated as '
- 'active. Unselect this instead of deleting accounts.'))
is_superuser = models.BooleanField(_('superuser status'), default=False,
help_text=_('Designates that this user has all permissions without '
'explicitly assigning them.'))
- date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
groups = models.ManyToManyField(Group, verbose_name=_('groups'),
blank=True, help_text=_('The groups this user belongs to. A user will '
'get all permissions granted to each of '
@@ -322,30 +306,9 @@ class AbstractUser(AbstractBaseUser):
verbose_name=_('user permissions'), blank=True,
help_text='Specific permissions for this user.')
- objects = UserManager()
-
- USERNAME_FIELD = 'username'
- REQUIRED_FIELDS = ['email']
-
class Meta:
- verbose_name = _('user')
- verbose_name_plural = _('users')
abstract = True
- def get_absolute_url(self):
- return "/users/%s/" % urlquote(self.username)
-
- def get_full_name(self):
- """
- Returns the first_name plus the last_name, with a space in between.
- """
- full_name = '%s %s' % (self.first_name, self.last_name)
- return full_name.strip()
-
- def get_short_name(self):
- "Returns the short name for the user."
- return self.first_name
-
def get_group_permissions(self, obj=None):
"""
Returns a list of permission strings that this user has through his/her
@@ -403,6 +366,55 @@ class AbstractUser(AbstractBaseUser):
return _user_has_module_perms(self, app_label)
+
+class AbstractUser(AbstractBaseUser, PermissionsMixin):
+ """
+ An abstract base class implementing a fully featured User model with
+ admin-compliant permissions.
+
+ Username, password and email are required. Other fields are optional.
+ """
+ username = models.CharField(_('username'), max_length=30, unique=True,
+ help_text=_('Required. 30 characters or fewer. Letters, numbers and '
+ '@/./+/-/_ characters'),
+ validators=[
+ validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
+ ])
+ first_name = models.CharField(_('first name'), max_length=30, blank=True)
+ last_name = models.CharField(_('last name'), max_length=30, blank=True)
+ email = models.EmailField(_('email address'), blank=True)
+ is_staff = models.BooleanField(_('staff status'), default=False,
+ help_text=_('Designates whether the user can log into this admin '
+ 'site.'))
+ is_active = models.BooleanField(_('active'), default=True,
+ help_text=_('Designates whether this user should be treated as '
+ 'active. Unselect this instead of deleting accounts.'))
+ date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
+
+ objects = UserManager()
+
+ USERNAME_FIELD = 'username'
+ REQUIRED_FIELDS = ['email']
+
+ class Meta:
+ verbose_name = _('user')
+ verbose_name_plural = _('users')
+ abstract = True
+
+ def get_absolute_url(self):
+ return "/users/%s/" % urlquote(self.username)
+
+ def get_full_name(self):
+ """
+ Returns the first_name plus the last_name, with a space in between.
+ """
+ full_name = '%s %s' % (self.first_name, self.last_name)
+ return full_name.strip()
+
+ def get_short_name(self):
+ "Returns the short name for the user."
+ return self.first_name
+
def email_user(self, subject, message, from_email=None):
"""
Sends an email to this User.
diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py
index b3007ea484..038c8980d9 100644
--- a/django/contrib/auth/tests/__init__.py
+++ b/django/contrib/auth/tests/__init__.py
@@ -14,3 +14,16 @@ from django.contrib.auth.tests.tokens import *
from django.contrib.auth.tests.views import *
# The password for the fixture data users is 'password'
+
+from django.dispatch import receiver
+from django.test.signals import setting_changed
+
+
+@receiver(setting_changed)
+def user_model_swapped(**kwargs):
+ if kwargs['setting'] == 'AUTH_USER_MODEL':
+ from django.db.models.manager import ensure_default_manager
+ from django.contrib.auth.models import User
+ # Reset User manager
+ setattr(User, 'objects', User._default_manager)
+ ensure_default_manager(User)
diff --git a/django/contrib/auth/tests/auth_backends.py b/django/contrib/auth/tests/auth_backends.py
index e92f159ff9..71f18d32cf 100644
--- a/django/contrib/auth/tests/auth_backends.py
+++ b/django/contrib/auth/tests/auth_backends.py
@@ -4,9 +4,10 @@ from datetime import date
from django.conf import settings
from django.contrib.auth.models import User, Group, Permission, AnonymousUser
from django.contrib.auth.tests.utils import skipIfCustomUser
-from django.contrib.auth.tests.custom_user import ExtensionUser
+from django.contrib.auth.tests.custom_user import ExtensionUser, CustomPermissionsUser
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ImproperlyConfigured
+from django.core.exceptions import ImproperlyConfigured, PermissionDenied
+from django.contrib.auth import authenticate
from django.test import TestCase
from django.test.utils import override_settings
@@ -33,7 +34,7 @@ class BaseModelBackendTest(object):
ContentType.objects.clear_cache()
def test_has_perm(self):
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
self.assertEqual(user.has_perm('auth.test'), False)
user.is_staff = True
user.save()
@@ -52,14 +53,14 @@ class BaseModelBackendTest(object):
self.assertEqual(user.has_perm('auth.test'), False)
def test_custom_perms(self):
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
content_type = ContentType.objects.get_for_model(Group)
perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
user.user_permissions.add(perm)
user.save()
# reloading user to purge the _perm_cache
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
self.assertEqual(user.get_all_permissions() == set(['auth.test']), True)
self.assertEqual(user.get_group_permissions(), set([]))
self.assertEqual(user.has_module_perms('Group'), False)
@@ -70,7 +71,7 @@ class BaseModelBackendTest(object):
perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3')
user.user_permissions.add(perm)
user.save()
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
self.assertEqual(user.get_all_permissions(), set(['auth.test2', 'auth.test', 'auth.test3']))
self.assertEqual(user.has_perm('test'), False)
self.assertEqual(user.has_perm('auth.test'), True)
@@ -80,7 +81,7 @@ class BaseModelBackendTest(object):
group.permissions.add(perm)
group.save()
user.groups.add(group)
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
exp = set(['auth.test2', 'auth.test', 'auth.test3', 'auth.test_group'])
self.assertEqual(user.get_all_permissions(), exp)
self.assertEqual(user.get_group_permissions(), set(['auth.test_group']))
@@ -92,7 +93,7 @@ class BaseModelBackendTest(object):
def test_has_no_object_perm(self):
"""Regressiontest for #12462"""
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
content_type = ContentType.objects.get_for_model(Group)
perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
user.user_permissions.add(perm)
@@ -105,7 +106,7 @@ class BaseModelBackendTest(object):
def test_get_all_superuser_permissions(self):
"A superuser has all permissions. Refs #14795"
- user = self.UserModel.objects.get(username='test2')
+ user = self.UserModel.objects.get(pk=self.superuser.pk)
self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all()))
@@ -117,12 +118,12 @@ class ModelBackendTest(BaseModelBackendTest, TestCase):
UserModel = User
def create_users(self):
- User.objects.create_user(
+ self.user = User.objects.create_user(
username='test',
email='test@example.com',
password='test',
)
- User.objects.create_superuser(
+ self.superuser = User.objects.create_superuser(
username='test2',
email='test2@example.com',
password='test',
@@ -150,13 +151,13 @@ class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
UserModel = ExtensionUser
def create_users(self):
- ExtensionUser.objects.create_user(
+ self.user = ExtensionUser.objects.create_user(
username='test',
email='test@example.com',
password='test',
date_of_birth=date(2006, 4, 25)
)
- ExtensionUser.objects.create_superuser(
+ self.superuser = ExtensionUser.objects.create_superuser(
username='test2',
email='test2@example.com',
password='test',
@@ -164,6 +165,31 @@ class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
)
+@override_settings(AUTH_USER_MODEL='auth.CustomPermissionsUser')
+class CustomPermissionsUserModelBackendTest(BaseModelBackendTest, TestCase):
+ """
+ Tests for the ModelBackend using the CustomPermissionsUser model.
+
+ As with the ExtensionUser test, this isn't a perfect test, because both
+ the User and CustomPermissionsUser are synchronized to the database,
+ which wouldn't ordinary happen in production.
+ """
+
+ UserModel = CustomPermissionsUser
+
+ def create_users(self):
+ self.user = CustomPermissionsUser.objects.create_user(
+ email='test@example.com',
+ password='test',
+ date_of_birth=date(2006, 4, 25)
+ )
+ self.superuser = CustomPermissionsUser.objects.create_superuser(
+ email='test2@example.com',
+ password='test',
+ date_of_birth=date(1976, 11, 8)
+ )
+
+
class TestObj(object):
pass
@@ -323,3 +349,38 @@ class InActiveUserBackendTest(TestCase):
def test_has_module_perms(self):
self.assertEqual(self.user1.has_module_perms("app1"), False)
self.assertEqual(self.user1.has_module_perms("app2"), False)
+
+
+class PermissionDeniedBackend(object):
+ """
+ Always raises PermissionDenied.
+ """
+ supports_object_permissions = True
+ supports_anonymous_user = True
+ supports_inactive_user = True
+
+ def authenticate(self, username=None, password=None):
+ raise PermissionDenied
+
+
+@skipIfCustomUser
+class PermissionDeniedBackendTest(TestCase):
+ """
+ Tests that other backends are not checked once a backend raises PermissionDenied
+ """
+ backend = 'django.contrib.auth.tests.auth_backends.PermissionDeniedBackend'
+
+ def setUp(self):
+ self.user1 = User.objects.create_user('test', 'test@example.com', 'test')
+ self.user1.save()
+
+ @override_settings(AUTHENTICATION_BACKENDS=(backend, ) +
+ tuple(settings.AUTHENTICATION_BACKENDS))
+ def test_permission_denied(self):
+ "user is not authenticated after a backend raises permission denied #2550"
+ self.assertEqual(authenticate(username='test', password='test'), None)
+
+ @override_settings(AUTHENTICATION_BACKENDS=tuple(
+ settings.AUTHENTICATION_BACKENDS) + (backend, ))
+ def test_authenticates(self):
+ self.assertEqual(authenticate(username='test', password='test'), self.user1)
diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py
index bc7344f753..2c807cca51 100644
--- a/django/contrib/auth/tests/basic.py
+++ b/django/contrib/auth/tests/basic.py
@@ -162,6 +162,8 @@ class BasicTestCase(TestCase):
def test_swappable_user(self):
"The current user model can be swapped out for another"
self.assertEqual(get_user_model(), CustomUser)
+ with self.assertRaises(AttributeError):
+ User.objects.all()
@override_settings(AUTH_USER_MODEL='badsetting')
def test_swappable_user_bad_setting(self):
diff --git a/django/contrib/auth/tests/context_processors.py b/django/contrib/auth/tests/context_processors.py
index 32fea8ac80..f846a828dd 100644
--- a/django/contrib/auth/tests/context_processors.py
+++ b/django/contrib/auth/tests/context_processors.py
@@ -9,6 +9,7 @@ from django.contrib.auth.context_processors import PermWrapper, PermLookupDict
from django.db.models import Q
from django.test import TestCase
from django.test.utils import override_settings
+from django.utils._os import upath
class MockUser(object):
@@ -63,7 +64,7 @@ class PermWrapperTests(TestCase):
@skipIfCustomUser
@override_settings(
TEMPLATE_DIRS=(
- os.path.join(os.path.dirname(__file__), 'templates'),
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
),
USE_TZ=False, # required for loading the fixture
PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py
index a29ed6a104..7e042e4895 100644
--- a/django/contrib/auth/tests/custom_user.py
+++ b/django/contrib/auth/tests/custom_user.py
@@ -1,5 +1,11 @@
from django.db import models
-from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, AbstractUser, UserManager
+from django.contrib.auth.models import (
+ BaseUserManager,
+ AbstractBaseUser,
+ AbstractUser,
+ UserManager,
+ PermissionsMixin
+)
# The custom User uses email as the unique identifier, and requires
@@ -88,3 +94,53 @@ class ExtensionUser(AbstractUser):
class Meta:
app_label = 'auth'
+
+
+# The CustomPermissionsUser users email as the identifier, but uses the normal
+# Django permissions model. This allows us to check that the PermissionsMixin
+# includes everything that is needed to interact with the ModelBackend.
+
+class CustomPermissionsUserManager(CustomUserManager):
+ def create_superuser(self, email, password, date_of_birth):
+ u = self.create_user(email, password=password, date_of_birth=date_of_birth)
+ u.is_superuser = True
+ u.save(using=self._db)
+ return u
+
+
+class CustomPermissionsUser(AbstractBaseUser, PermissionsMixin):
+ email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
+ date_of_birth = models.DateField()
+
+ objects = CustomPermissionsUserManager()
+
+ USERNAME_FIELD = 'email'
+ REQUIRED_FIELDS = ['date_of_birth']
+
+ class Meta:
+ app_label = 'auth'
+
+ def get_full_name(self):
+ return self.email
+
+ def get_short_name(self):
+ return self.email
+
+ def __unicode__(self):
+ return self.email
+
+
+class IsActiveTestUser1(AbstractBaseUser):
+ """
+ This test user class and derivatives test the default is_active behavior
+ """
+ username = models.CharField(max_length=30, unique=True)
+
+ objects = BaseUserManager()
+
+ USERNAME_FIELD = 'username'
+
+ class Meta:
+ app_label = 'auth'
+
+ # the is_active attr is provided by AbstractBaseUser
diff --git a/django/contrib/auth/tests/forms.py b/django/contrib/auth/tests/forms.py
index f3eb24287e..a9f894905a 100644
--- a/django/contrib/auth/tests/forms.py
+++ b/django/contrib/auth/tests/forms.py
@@ -3,13 +3,15 @@ from __future__ import unicode_literals
import os
from django.contrib.auth.models import User
from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
- PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm)
+ PasswordChangeForm, SetPasswordForm, UserChangeForm, PasswordResetForm,
+ ReadOnlyPasswordHashWidget)
from django.contrib.auth.tests.utils import skipIfCustomUser
from django.core import mail
from django.forms.fields import Field, EmailField
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.encoding import force_text
+from django.utils._os import upath
from django.utils import translation
from django.utils.translation import ugettext as _
@@ -98,7 +100,9 @@ class AuthenticationFormTest(TestCase):
form = AuthenticationForm(None, data)
self.assertFalse(form.is_valid())
self.assertEqual(form.non_field_errors(),
- [force_text(form.error_messages['invalid_login'])])
+ [force_text(form.error_messages['invalid_login'] % {
+ 'username': User._meta.get_field('username').verbose_name
+ })])
def test_inactive_user(self):
# The user is inactive.
@@ -282,6 +286,14 @@ class UserChangeFormTest(TestCase):
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data['password'], 'sha1$6efc0$f93efe9fd7542f25a7be94871ea45aa95de57161')
+ def test_bug_19349_bound_password_field(self):
+ user = User.objects.get(username='testclient')
+ form = UserChangeForm(data={}, instance=user)
+ # When rendering the bound password field,
+ # ReadOnlyPasswordHashWidget needs the initial
+ # value to render correctly
+ self.assertEqual(form.initial['password'], form['password'].value())
+
@skipIfCustomUser
@override_settings(USE_TZ=False, PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
@@ -322,7 +334,7 @@ class PasswordResetFormTest(TestCase):
self.assertEqual(form.cleaned_data['email'], email)
def test_custom_email_subject(self):
- template_path = os.path.join(os.path.dirname(__file__), 'templates')
+ template_path = os.path.join(os.path.dirname(upath(__file__)), 'templates')
with self.settings(TEMPLATE_DIRS=(template_path,)):
data = {'email': 'testclient@example.com'}
form = PasswordResetForm(data)
@@ -362,3 +374,13 @@ class PasswordResetFormTest(TestCase):
self.assertFalse(form.is_valid())
self.assertEqual(form["email"].errors,
[_("The user account associated with this email address cannot reset the password.")])
+
+
+class ReadOnlyPasswordHashWidgetTest(TestCase):
+
+ def test_bug_19349_render_with_none_value(self):
+ # Rendering the widget with value set to None
+ # mustn't raise an exception.
+ widget = ReadOnlyPasswordHashWidget()
+ html = widget.render(name='password', value=None, attrs={})
+ self.assertIn(_("No password set."), html)
diff --git a/django/contrib/auth/tests/handlers.py b/django/contrib/auth/tests/handlers.py
index a867aae47a..04ab46f75b 100644
--- a/django/contrib/auth/tests/handlers.py
+++ b/django/contrib/auth/tests/handlers.py
@@ -2,30 +2,23 @@ from __future__ import unicode_literals
from django.contrib.auth.handlers.modwsgi import check_password, groups_for_user
from django.contrib.auth.models import User, Group
+from django.contrib.auth.tests import CustomUser
from django.contrib.auth.tests.utils import skipIfCustomUser
from django.test import TransactionTestCase
+from django.test.utils import override_settings
class ModWsgiHandlerTestCase(TransactionTestCase):
"""
Tests for the mod_wsgi authentication handler
"""
-
- def setUp(self):
- user1 = User.objects.create_user('test', 'test@example.com', 'test')
- User.objects.create_user('test1', 'test1@example.com', 'test1')
- group = Group.objects.create(name='test_group')
- user1.groups.add(group)
-
+ @skipIfCustomUser
def test_check_password(self):
"""
Verify that check_password returns the correct values as per
http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider
-
- because the custom user available in the test framework does not
- support the is_active attribute, we can't test this with a custom
- user.
"""
+ User.objects.create_user('test', 'test@example.com', 'test')
# User not in database
self.assertTrue(check_password({}, 'unknown', '') is None)
@@ -33,15 +26,43 @@ class ModWsgiHandlerTestCase(TransactionTestCase):
# Valid user with correct password
self.assertTrue(check_password({}, 'test', 'test'))
+ # correct password, but user is inactive
+ User.objects.filter(username='test').update(is_active=False)
+ self.assertFalse(check_password({}, 'test', 'test'))
+
# Valid user with incorrect password
self.assertFalse(check_password({}, 'test', 'incorrect'))
+ @override_settings(AUTH_USER_MODEL='auth.CustomUser')
+ def test_check_password_custom_user(self):
+ """
+ Verify that check_password returns the correct values as per
+ http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Authentication_Provider
+
+ with custom user installed
+ """
+
+ CustomUser.objects.create_user('test@example.com', '1990-01-01', 'test')
+
+ # User not in database
+ self.assertTrue(check_password({}, 'unknown', '') is None)
+
+ # Valid user with correct password'
+ self.assertTrue(check_password({}, 'test@example.com', 'test'))
+
+ # Valid user with incorrect password
+ self.assertFalse(check_password({}, 'test@example.com', 'incorrect'))
+
@skipIfCustomUser
def test_groups_for_user(self):
"""
Check that groups_for_user returns correct values as per
http://code.google.com/p/modwsgi/wiki/AccessControlMechanisms#Apache_Group_Authorisation
"""
+ user1 = User.objects.create_user('test', 'test@example.com', 'test')
+ User.objects.create_user('test1', 'test1@example.com', 'test1')
+ group = Group.objects.create(name='test_group')
+ user1.groups.add(group)
# User not in database
self.assertEqual(groups_for_user({}, 'unknown'), [])
diff --git a/django/contrib/auth/tests/models.py b/django/contrib/auth/tests/models.py
index 252a0887c8..ca65dee71b 100644
--- a/django/contrib/auth/tests/models.py
+++ b/django/contrib/auth/tests/models.py
@@ -1,4 +1,5 @@
from django.conf import settings
+from django.contrib.auth import get_user_model
from django.contrib.auth.models import (Group, User, SiteProfileNotAvailable,
UserManager)
from django.contrib.auth.tests.utils import skipIfCustomUser
@@ -98,3 +99,36 @@ class UserManagerTestCase(TestCase):
self.assertRaisesMessage(ValueError,
'The given username must be set',
User.objects.create_user, username='')
+
+
+class IsActiveTestCase(TestCase):
+ """
+ Tests the behavior of the guaranteed is_active attribute
+ """
+
+ @skipIfCustomUser
+ def test_builtin_user_isactive(self):
+ user = User.objects.create(username='foo', email='foo@bar.com')
+ # is_active is true by default
+ self.assertEqual(user.is_active, True)
+ user.is_active = False
+ user.save()
+ user_fetched = User.objects.get(pk=user.pk)
+ # the is_active flag is saved
+ self.assertFalse(user_fetched.is_active)
+
+ @override_settings(AUTH_USER_MODEL='auth.IsActiveTestUser1')
+ def test_is_active_field_default(self):
+ """
+ tests that the default value for is_active is provided
+ """
+ UserModel = get_user_model()
+ user = UserModel(username='foo')
+ self.assertEqual(user.is_active, True)
+ # you can set the attribute - but it will not save
+ user.is_active = False
+ # there should be no problem saving - but the attribute is not saved
+ user.save()
+ user_fetched = UserModel.objects.get(pk=user.pk)
+ # the attribute is always true for newly retrieved instance
+ self.assertEqual(user_fetched.is_active, True)
diff --git a/django/contrib/auth/tests/remote_user.py b/django/contrib/auth/tests/remote_user.py
index 9b0f6f8be3..0e59b291a8 100644
--- a/django/contrib/auth/tests/remote_user.py
+++ b/django/contrib/auth/tests/remote_user.py
@@ -1,8 +1,9 @@
from datetime import datetime
from django.conf import settings
+from django.contrib.auth import authenticate
from django.contrib.auth.backends import RemoteUserBackend
-from django.contrib.auth.models import User
+from django.contrib.auth.models import User, AnonymousUser
from django.contrib.auth.tests.utils import skipIfCustomUser
from django.test import TestCase
from django.utils import timezone
@@ -23,7 +24,7 @@ class RemoteUserTest(TestCase):
self.curr_middleware = settings.MIDDLEWARE_CLASSES
self.curr_auth = settings.AUTHENTICATION_BACKENDS
settings.MIDDLEWARE_CLASSES += (self.middleware,)
- settings.AUTHENTICATION_BACKENDS = (self.backend,)
+ settings.AUTHENTICATION_BACKENDS += (self.backend,)
def test_no_remote_user(self):
"""
@@ -97,6 +98,26 @@ class RemoteUserTest(TestCase):
response = self.client.get('/remote_user/', REMOTE_USER=self.known_user)
self.assertEqual(default_login, response.context['user'].last_login)
+ def test_header_disappears(self):
+ """
+ Tests that a logged in user is logged out automatically when
+ the REMOTE_USER header disappears during the same browser session.
+ """
+ User.objects.create(username='knownuser')
+ # Known user authenticates
+ response = self.client.get('/remote_user/', REMOTE_USER=self.known_user)
+ self.assertEqual(response.context['user'].username, 'knownuser')
+ # During the session, the REMOTE_USER header disappears. Should trigger logout.
+ response = self.client.get('/remote_user/')
+ self.assertEqual(response.context['user'].is_anonymous(), True)
+ # verify the remoteuser middleware will not remove a user
+ # authenticated via another backend
+ User.objects.create_user(username='modeluser', password='foo')
+ self.client.login(username='modeluser', password='foo')
+ authenticate(username='modeluser', password='foo')
+ response = self.client.get('/remote_user/')
+ self.assertEqual(response.context['user'].username, 'modeluser')
+
def tearDown(self):
"""Restores settings to avoid breaking other tests."""
settings.MIDDLEWARE_CLASSES = self.curr_middleware
diff --git a/django/contrib/auth/tests/templates/context_processors/auth_attrs_user.html b/django/contrib/auth/tests/templates/context_processors/auth_attrs_user.html
index aa7f784405..dc4c6b17c1 100644
--- a/django/contrib/auth/tests/templates/context_processors/auth_attrs_user.html
+++ b/django/contrib/auth/tests/templates/context_processors/auth_attrs_user.html
@@ -1,4 +1,4 @@
unicode: {{ user }}
-id: {{ user.id }}
+id: {{ user.pk }}
username: {{ user.username }}
url: {% url 'userpage' user %}
diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py
index bb17576d31..6040a2f5b5 100644
--- a/django/contrib/auth/tests/views.py
+++ b/django/contrib/auth/tests/views.py
@@ -11,6 +11,7 @@ from django.http import QueryDict
from django.utils.encoding import force_text
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
@@ -27,7 +28,7 @@ from django.contrib.auth.tests.utils import skipIfCustomUser
LANGUAGE_CODE='en',
TEMPLATE_LOADERS=global_settings.TEMPLATE_LOADERS,
TEMPLATE_DIRS=(
- os.path.join(os.path.dirname(__file__), 'templates'),
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
),
USE_TZ=False,
PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
@@ -115,6 +116,8 @@ class PasswordResetTest(AuthViewsTestCase):
self.assertTrue("http://adminsite.com" in mail.outbox[0].body)
self.assertEqual(settings.DEFAULT_FROM_EMAIL, mail.outbox[0].from_email)
+ # Skip any 500 handler action (like sending more mail...)
+ @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
def test_poisoned_http_host(self):
"Poisoned HTTP_HOST headers can't be used for reset emails"
# This attack is based on the way browsers handle URLs. The colon
@@ -131,6 +134,8 @@ class PasswordResetTest(AuthViewsTestCase):
)
self.assertEqual(len(mail.outbox), 0)
+ # 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):
@@ -243,7 +248,9 @@ class ChangePasswordTest(AuthViewsTestCase):
'username': 'testclient',
'password': password,
})
- self.assertContainsEscaped(response, AuthenticationForm.error_messages['invalid_login'])
+ self.assertContainsEscaped(response, AuthenticationForm.error_messages['invalid_login'] % {
+ 'username': User._meta.get_field('username').verbose_name
+ })
def logout(self):
response = self.client.get('/logout/')
diff --git a/django/contrib/auth/tokens.py b/django/contrib/auth/tokens.py
index 930c70012b..6e5bfe7d9d 100644
--- a/django/contrib/auth/tokens.py
+++ b/django/contrib/auth/tokens.py
@@ -58,7 +58,7 @@ class PasswordResetTokenGenerator(object):
# Ensure results are consistent across DB backends
login_timestamp = user.last_login.replace(microsecond=0, tzinfo=None)
- value = (six.text_type(user.id) + user.password +
+ value = (six.text_type(user.pk) + user.password +
six.text_type(login_timestamp) + six.text_type(timestamp))
hash = salted_hmac(key_salt, value).hexdigest()[::2]
return "%s-%s" % (ts_b36, hash)
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index d27e2f5aba..8514345d00 100644
--- a/django/contrib/auth/views.py
+++ b/django/contrib/auth/views.py
@@ -7,7 +7,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, QueryDict
from django.template.response import TemplateResponse
-from django.utils.http import base36_to_int
+from django.utils.http import base36_to_int, is_safe_url
from django.utils.translation import ugettext as _
from django.shortcuts import resolve_url
from django.views.decorators.debug import sensitive_post_parameters
@@ -37,18 +37,12 @@ def login(request, template_name='registration/login.html',
if request.method == "POST":
form = authentication_form(data=request.POST)
if form.is_valid():
- # Use default setting if redirect_to is empty
- if not redirect_to:
- redirect_to = settings.LOGIN_REDIRECT_URL
- redirect_to = resolve_url(redirect_to)
- netloc = urlparse(redirect_to)[1]
- # Heavier security check -- don't allow redirection to a different
- # host.
- if netloc and netloc != request.get_host():
+ # Ensure the user-originating redirection url is safe.
+ if not is_safe_url(url=redirect_to, host=request.get_host()):
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
- # Okay, security checks complete. Log the user in.
+ # Okay, security check complete. Log the user in.
auth_login(request, form.get_user())
if request.session.test_cookie_worked():
@@ -82,27 +76,27 @@ def logout(request, next_page=None,
Logs out the user and displays 'You are logged out' message.
"""
auth_logout(request)
- redirect_to = request.REQUEST.get(redirect_field_name, '')
- if redirect_to:
- netloc = urlparse(redirect_to)[1]
+
+ if redirect_field_name in request.REQUEST:
+ next_page = request.REQUEST[redirect_field_name]
# Security check -- don't allow redirection to a different host.
- if not (netloc and netloc != request.get_host()):
- return HttpResponseRedirect(redirect_to)
+ if not is_safe_url(url=next_page, host=request.get_host()):
+ next_page = request.path
- if next_page is None:
- current_site = get_current_site(request)
- context = {
- 'site': current_site,
- 'site_name': current_site.name,
- 'title': _('Logged out')
- }
- if extra_context is not None:
- context.update(extra_context)
- return TemplateResponse(request, template_name, context,
- current_app=current_app)
- else:
+ if next_page:
# Redirect to this page until the session has been cleared.
- return HttpResponseRedirect(next_page or request.path)
+ return HttpResponseRedirect(next_page)
+
+ current_site = get_current_site(request)
+ context = {
+ 'site': current_site,
+ 'site_name': current_site.name,
+ 'title': _('Logged out')
+ }
+ if extra_context is not None:
+ context.update(extra_context)
+ return TemplateResponse(request, template_name, context,
+ current_app=current_app)
def logout_then_login(request, login_url=None, current_app=None, extra_context=None):
@@ -206,7 +200,7 @@ def password_reset_confirm(request, uidb36=None, token=None,
post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete')
try:
uid_int = base36_to_int(uidb36)
- user = UserModel.objects.get(id=uid_int)
+ user = UserModel.objects.get(pk=uid_int)
except (ValueError, OverflowError, UserModel.DoesNotExist):
user = None
diff --git a/django/contrib/comments/__init__.py b/django/contrib/comments/__init__.py
index 42384e786b..1798c1adb5 100644
--- a/django/contrib/comments/__init__.py
+++ b/django/contrib/comments/__init__.py
@@ -20,9 +20,9 @@ def get_comment_app():
# Try to import the package
try:
package = import_module(comments_app)
- except ImportError:
+ except ImportError as e:
raise ImproperlyConfigured("The COMMENTS_APP setting refers to "\
- "a non-existing package.")
+ "a non-existing package. (%s)" % e)
return package
diff --git a/django/contrib/comments/signals.py b/django/contrib/comments/signals.py
index fe1083bd14..079afaf03a 100644
--- a/django/contrib/comments/signals.py
+++ b/django/contrib/comments/signals.py
@@ -6,7 +6,7 @@ from django.dispatch import Signal
# Sent just before a comment will be posted (after it's been approved and
# moderated; this can be used to modify the comment (in place) with posting
# details or other such actions. If any receiver returns False the comment will be
-# discarded and a 403 (not allowed) response. This signal is sent at more or less
+# discarded and a 400 response. This signal is sent at more or less
# the same time (just before, actually) as the Comment object's pre-save signal,
# except that the HTTP request is sent along with this signal.
comment_will_be_posted = Signal(providing_args=["comment", "request"])
diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py
index 27d5a48ac6..7c02b21b6a 100644
--- a/django/contrib/comments/views/comments.py
+++ b/django/contrib/comments/views/comments.py
@@ -44,9 +44,6 @@ def post_comment(request, next=None, using=None):
if not data.get('email', ''):
data["email"] = request.user.email
- # Check to see if the POST data overrides the view's next argument.
- next = data.get("next", next)
-
# Look up the object we're trying to comment about
ctype = data.get("content_type")
object_pk = data.get("object_pk")
@@ -100,7 +97,7 @@ def post_comment(request, next=None, using=None):
template_list, {
"comment": form.data.get("comment", ""),
"form": form,
- "next": next,
+ "next": data.get("next", next),
},
RequestContext(request, {})
)
@@ -131,7 +128,8 @@ def post_comment(request, next=None, using=None):
request=request
)
- return next_redirect(data, next, comment_done, c=comment._get_pk_val())
+ return next_redirect(request, fallback=next or 'comments-comment-done',
+ c=comment._get_pk_val())
comment_done = confirmation_view(
template="comments/posted.html",
diff --git a/django/contrib/comments/views/moderation.py b/django/contrib/comments/views/moderation.py
index 39933e75c8..31bb98fa63 100644
--- a/django/contrib/comments/views/moderation.py
+++ b/django/contrib/comments/views/moderation.py
@@ -10,7 +10,6 @@ from django.shortcuts import get_object_or_404, render_to_response
from django.views.decorators.csrf import csrf_protect
-
@csrf_protect
@login_required
def flag(request, comment_id, next=None):
@@ -27,7 +26,8 @@ def flag(request, comment_id, next=None):
# Flag on POST
if request.method == 'POST':
perform_flag(request, comment)
- return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk)
+ return next_redirect(request, fallback=next or 'comments-flag-done',
+ c=comment.pk)
# Render a form on GET
else:
@@ -54,7 +54,8 @@ def delete(request, comment_id, next=None):
if request.method == 'POST':
# Flag the comment as deleted instead of actually deleting it.
perform_delete(request, comment)
- return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk)
+ return next_redirect(request, fallback=next or 'comments-delete-done',
+ c=comment.pk)
# Render a form on GET
else:
@@ -81,7 +82,8 @@ def approve(request, comment_id, next=None):
if request.method == 'POST':
# Flag the comment as approved.
perform_approve(request, comment)
- return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)
+ return next_redirect(request, fallback=next or 'comments-approve-done',
+ c=comment.pk)
# Render a form on GET
else:
diff --git a/django/contrib/comments/views/utils.py b/django/contrib/comments/views/utils.py
index abaed68560..79f6376232 100644
--- a/django/contrib/comments/views/utils.py
+++ b/django/contrib/comments/views/utils.py
@@ -9,25 +9,26 @@ except ImportError: # Python 2
from urllib import urlencode
from django.http import HttpResponseRedirect
-from django.core import urlresolvers
-from django.shortcuts import render_to_response
+from django.shortcuts import render_to_response, resolve_url
from django.template import RequestContext
from django.core.exceptions import ObjectDoesNotExist
from django.contrib import comments
+from django.utils.http import is_safe_url
-def next_redirect(data, default, default_view, **get_kwargs):
+def next_redirect(request, fallback, **get_kwargs):
"""
Handle the "where should I go next?" part of comment views.
- The next value could be a kwarg to the function (``default``), or a
- ``?next=...`` GET arg, or the URL of a given view (``default_view``). See
+ The next value could be a
+ ``?next=...`` GET arg or the URL of a given view (``fallback``). See
the view modules for examples.
Returns an ``HttpResponseRedirect``.
"""
- next = data.get("next", default)
- if next is None:
- next = urlresolvers.reverse(default_view)
+ next = request.POST.get('next')
+ if not is_safe_url(url=next, host=request.get_host()):
+ next = resolve_url(fallback)
+
if get_kwargs:
if '#' in next:
tmp = next.rsplit('#', 1)
diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
index 29e93eefe7..6aff07e568 100644
--- a/django/contrib/contenttypes/generic.py
+++ b/django/contrib/contenttypes/generic.py
@@ -5,20 +5,19 @@ from __future__ import unicode_literals
from collections import defaultdict
from functools import partial
-from operator import attrgetter
from django.core.exceptions import ObjectDoesNotExist
from django.db import connection
from django.db.models import signals
from django.db import models, router, DEFAULT_DB_ALIAS
from django.db.models.fields.related import RelatedField, Field, ManyToManyRel
-from django.db.models.loading import get_model
from django.forms import ModelForm
from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance
from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import smart_text
+
class GenericForeignKey(object):
"""
Provides a generic relation to any object through content-type/object-id
@@ -52,9 +51,6 @@ class GenericForeignKey(object):
kwargs[self.fk_field] = value._get_pk_val()
def get_content_type(self, obj=None, id=None, using=None):
- # Convenience function using get_model avoids a circular import when
- # using this model
- ContentType = get_model("contenttypes", "contenttype")
if obj:
return ContentType.objects.db_manager(obj._state.db).get_for_model(obj)
elif id:
@@ -209,18 +205,16 @@ class GenericRelation(RelatedField, Field):
# same db_type as well.
return None
- def extra_filters(self, pieces, pos, negate):
+ def get_content_type(self):
"""
- Return an extra filter to the queryset so that the results are filtered
- on the appropriate content type.
+ Returns the content type associated with this field's model.
"""
- if negate:
- return []
- ContentType = get_model("contenttypes", "contenttype")
- content_type = ContentType.objects.get_for_model(self.model)
- prefix = "__".join(pieces[:pos + 1])
- return [("%s__%s" % (prefix, self.content_type_field_name),
- content_type)]
+ return ContentType.objects.get_for_model(self.model)
+
+ def get_extra_join_sql(self, connection, qn, lhs_alias, rhs_alias):
+ extra_col = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0].column
+ contenttype = self.get_content_type().pk
+ return " AND %s.%s = %%s" % (qn(rhs_alias), qn(extra_col)), [contenttype]
def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS):
"""
@@ -251,9 +245,6 @@ class ReverseGenericRelatedObjectsDescriptor(object):
if instance is None:
return self
- # This import is done here to avoid circular import importing this module
- from django.contrib.contenttypes.models import ContentType
-
# Dynamically create a class that subclasses the related model's
# default manager.
rel_model = self.field.rel.to
@@ -329,8 +320,11 @@ def create_generic_related_manager(superclass):
set(obj._get_pk_val() for obj in instances)
}
qs = super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**query)
+ # We (possibly) need to convert object IDs to the type of the
+ # instances' PK in order to match up instances:
+ object_id_converter = instances[0]._meta.pk.to_python
return (qs,
- attrgetter(self.object_id_field_name),
+ lambda relobj: object_id_converter(getattr(relobj, self.object_id_field_name)),
lambda obj: obj._get_pk_val(),
False,
self.prefetch_cache_name)
@@ -381,8 +375,6 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
def __init__(self, data=None, files=None, instance=None, save_as_new=None,
prefix=None, queryset=None):
- # Avoid a circular import.
- from django.contrib.contenttypes.models import ContentType
opts = self.model._meta
self.instance = instance
self.rel_name = '-'.join((
@@ -411,8 +403,6 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
))
def save_new(self, form, commit=True):
- # Avoid a circular import.
- from django.contrib.contenttypes.models import ContentType
kwargs = {
self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
self.ct_fk_field.get_attname(): self.instance.pk,
@@ -434,8 +424,6 @@ def generic_inlineformset_factory(model, form=ModelForm,
defaults ``content_type`` and ``object_id`` respectively.
"""
opts = model._meta
- # Avoid a circular import.
- from django.contrib.contenttypes.models import ContentType
# if there is no field called `ct_field` let the exception propagate
ct_field = opts.get_field(ct_field)
if not isinstance(ct_field, models.ForeignKey) or ct_field.rel.to != ContentType:
diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py
index 9f287d494b..8329ab65d9 100644
--- a/django/contrib/contenttypes/management.py
+++ b/django/contrib/contenttypes/management.py
@@ -1,14 +1,19 @@
from django.contrib.contenttypes.models import ContentType
+from django.db import DEFAULT_DB_ALIAS, router
from django.db.models import get_apps, get_models, signals
from django.utils.encoding import smart_text
from django.utils import six
from django.utils.six.moves import input
-def update_contenttypes(app, created_models, verbosity=2, **kwargs):
+
+def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, **kwargs):
"""
Creates content types for models in the given app, removing any model
entries that no longer have a matching model class.
"""
+ if not router.allow_syncdb(db, ContentType):
+ return
+
ContentType.objects.clear_cache()
app_models = get_models(app)
if not app_models:
@@ -19,10 +24,11 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
(model._meta.object_name.lower(), model)
for model in app_models
)
+
# Get all the content types
content_types = dict(
(ct.model, ct)
- for ct in ContentType.objects.filter(app_label=app_label)
+ for ct in ContentType.objects.using(db).filter(app_label=app_label)
)
to_remove = [
ct
@@ -30,7 +36,7 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
if model_name not in app_models
]
- cts = ContentType.objects.bulk_create([
+ cts = [
ContentType(
name=smart_text(model._meta.verbose_name_raw),
app_label=app_label,
@@ -38,7 +44,8 @@ def update_contenttypes(app, created_models, verbosity=2, **kwargs):
)
for (model_name, model) in six.iteritems(app_models)
if model_name not in content_types
- ])
+ ]
+ ContentType.objects.using(db).bulk_create(cts)
if verbosity >= 2:
for ct in cts:
print("Adding content type '%s | %s'" % (ct.app_label, ct.model))
@@ -71,6 +78,7 @@ If you're unsure, answer 'no'.
if verbosity >= 2:
print("Stale content types remain.")
+
def update_all_contenttypes(verbosity=2, **kwargs):
for app in get_apps():
update_contenttypes(app, None, verbosity, **kwargs)
diff --git a/django/contrib/flatpages/forms.py b/django/contrib/flatpages/forms.py
index e0a63e4323..a848875a9f 100644
--- a/django/contrib/flatpages/forms.py
+++ b/django/contrib/flatpages/forms.py
@@ -35,7 +35,7 @@ class FlatpageForm(forms.ModelForm):
for site in sites:
if same_url.filter(sites=site).exists():
raise forms.ValidationError(
- _('Flatpage with url %(url)s already exists for site %(site)s' %
- {'url': url, 'site': site}))
+ _('Flatpage with url %(url)s already exists for site %(site)s') %
+ {'url': url, 'site': site})
return super(FlatpageForm, self).clean()
diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py
index a21ffde533..aa7d5ff7d4 100644
--- a/django/contrib/formtools/tests/__init__.py
+++ b/django/contrib/formtools/tests/__init__.py
@@ -14,6 +14,7 @@ from django.contrib.formtools.wizard import FormWizard
from django.test import TestCase
from django.test.html import parse_html
from django.test.utils import override_settings
+from django.utils._os import upath
from django.utils import unittest
from django.contrib.formtools.tests.wizard import *
@@ -36,7 +37,7 @@ class TestFormPreview(preview.FormPreview):
@override_settings(
TEMPLATE_DIRS=(
- os.path.join(os.path.dirname(__file__), 'templates'),
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
),
)
class PreviewTests(TestCase):
@@ -214,7 +215,7 @@ class DummyRequest(http.HttpRequest):
@override_settings(
SECRET_KEY="123",
TEMPLATE_DIRS=(
- os.path.join(os.path.dirname(__file__), 'templates'),
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
),
)
class WizardTests(TestCase):
diff --git a/django/contrib/formtools/tests/wizard/wizardtests/tests.py b/django/contrib/formtools/tests/wizard/wizardtests/tests.py
index 586bd59341..4aaea7d56e 100644
--- a/django/contrib/formtools/tests/wizard/wizardtests/tests.py
+++ b/django/contrib/formtools/tests/wizard/wizardtests/tests.py
@@ -9,6 +9,7 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.formtools.wizard.views import CookieWizardView
from django.contrib.formtools.tests.wizard.forms import UserForm, UserFormSet
+from django.utils._os import upath
class WizardTests(object):
@@ -72,6 +73,10 @@ class WizardTests(object):
self.assertEqual(response.context['wizard']['steps'].current, 'form2')
self.assertEqual(response.context.get('another_var', None), True)
+ # ticket #19025: `form` should be included in context
+ form = response.context_data['wizard']['form']
+ self.assertEqual(response.context_data['form'], form)
+
def test_form_finish(self):
response = self.client.get(self.wizard_url)
self.assertEqual(response.status_code, 200)
@@ -82,7 +87,7 @@ class WizardTests(object):
self.assertEqual(response.context['wizard']['steps'].current, 'form2')
post_data = self.wizard_step_data[1]
- post_data['form2-file1'] = open(__file__, 'rb')
+ post_data['form2-file1'] = open(upath(__file__), 'rb')
response = self.client.post(self.wizard_url, post_data)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['wizard']['steps'].current, 'form3')
@@ -95,7 +100,7 @@ class WizardTests(object):
self.assertEqual(response.status_code, 200)
all_data = response.context['form_list']
- with open(__file__, 'rb') as f:
+ with open(upath(__file__), 'rb') as f:
self.assertEqual(all_data[1]['file1'].read(), f.read())
all_data[1]['file1'].close()
del all_data[1]['file1']
@@ -114,7 +119,7 @@ class WizardTests(object):
self.assertEqual(response.status_code, 200)
post_data = self.wizard_step_data[1]
- with open(__file__, 'rb') as post_file:
+ with open(upath(__file__), 'rb') as post_file:
post_data['form2-file1'] = post_file
response = self.client.post(self.wizard_url, post_data)
self.assertEqual(response.status_code, 200)
@@ -126,7 +131,7 @@ class WizardTests(object):
self.assertEqual(response.status_code, 200)
all_data = response.context['all_cleaned_data']
- with open(__file__, 'rb') as f:
+ with open(upath(__file__), 'rb') as f:
self.assertEqual(all_data['file1'].read(), f.read())
all_data['file1'].close()
del all_data['file1']
@@ -146,7 +151,7 @@ class WizardTests(object):
post_data = self.wizard_step_data[1]
post_data['form2-file1'].close()
- post_data['form2-file1'] = open(__file__, 'rb')
+ post_data['form2-file1'] = open(upath(__file__), 'rb')
response = self.client.post(self.wizard_url, post_data)
self.assertEqual(response.status_code, 200)
@@ -174,7 +179,7 @@ class WizardTests(object):
post_data = self.wizard_step_data[1]
post_data['form2-file1'].close()
- post_data['form2-file1'] = open(__file__, 'rb')
+ post_data['form2-file1'] = open(upath(__file__), 'rb')
response = self.client.post(self.wizard_url, post_data)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['wizard']['steps'].current, 'form3')
@@ -287,7 +292,7 @@ class WizardTestKwargs(TestCase):
self.wizard_step_data[0]['form1-user'] = self.testuser.pk
def test_template(self):
- templates = os.path.join(os.path.dirname(__file__), 'templates')
+ templates = os.path.join(os.path.dirname(upath(__file__)), 'templates')
with self.settings(
TEMPLATE_DIRS=list(settings.TEMPLATE_DIRS) + [templates]):
response = self.client.get(self.wizard_url)
diff --git a/django/contrib/formtools/wizard/storage/base.py b/django/contrib/formtools/wizard/storage/base.py
index aafc833484..2e59679d09 100644
--- a/django/contrib/formtools/wizard/storage/base.py
+++ b/django/contrib/formtools/wizard/storage/base.py
@@ -69,7 +69,9 @@ class BaseStorage(object):
wizard_files = self.data[self.step_files_key].get(step, {})
if wizard_files and not self.file_storage:
- raise NoFileStorageConfigured
+ raise NoFileStorageConfigured(
+ "You need to define 'file_storage' in your "
+ "wizard view in order to handle file uploads.")
files = {}
for field, field_dict in six.iteritems(wizard_files):
@@ -81,7 +83,9 @@ class BaseStorage(object):
def set_step_files(self, step, files):
if files and not self.file_storage:
- raise NoFileStorageConfigured
+ raise NoFileStorageConfigured(
+ "You need to define 'file_storage' in your "
+ "wizard view in order to handle file uploads.")
if step not in self.data[self.step_files_key]:
self.data[self.step_files_key][step] = {}
diff --git a/django/contrib/formtools/wizard/views.py b/django/contrib/formtools/wizard/views.py
index ea41e86852..cba39151e7 100644
--- a/django/contrib/formtools/wizard/views.py
+++ b/django/contrib/formtools/wizard/views.py
@@ -174,7 +174,9 @@ class WizardView(TemplateView):
for field in six.itervalues(form.base_fields):
if (isinstance(field, forms.FileField) and
not hasattr(cls, 'file_storage')):
- raise NoFileStorageConfigured
+ raise NoFileStorageConfigured(
+ "You need to define 'file_storage' in your "
+ "wizard view in order to handle file uploads.")
# build the kwargs for the wizardview instances
kwargs['form_list'] = init_form_list
@@ -436,8 +438,8 @@ class WizardView(TemplateView):
def get_all_cleaned_data(self):
"""
Returns a merged dictionary of all step cleaned_data dictionaries.
- If a step contains a `FormSet`, the key will be prefixed with formset
- and contain a list of the formset cleaned_data dictionaries.
+ If a step contains a `FormSet`, the key will be prefixed with
+ 'formset-' and contain a list of the formset cleaned_data dictionaries.
"""
cleaned_data = {}
for form_key in self.get_form_list():
@@ -458,8 +460,8 @@ class WizardView(TemplateView):
def get_cleaned_data_for_step(self, step):
"""
Returns the cleaned data for a given `step`. Before returning the
- cleaned data, the stored values are being revalidated through the
- form. If the data doesn't validate, None will be returned.
+ cleaned data, the stored values are revalidated through the form.
+ If the data doesn't validate, None will be returned.
"""
if step in self.form_list:
form_obj = self.get_form(step=step,
@@ -528,7 +530,7 @@ class WizardView(TemplateView):
context.update({'another_var': True})
return context
"""
- context = super(WizardView, self).get_context_data(**kwargs)
+ context = super(WizardView, self).get_context_data(form=form, **kwargs)
context.update(self.storage.extra_data)
context['wizard'] = {
'form': form,
diff --git a/django/contrib/gis/db/backends/oracle/compiler.py b/django/contrib/gis/db/backends/oracle/compiler.py
index f0eb5cad00..98da0163ba 100644
--- a/django/contrib/gis/db/backends/oracle/compiler.py
+++ b/django/contrib/gis/db/backends/oracle/compiler.py
@@ -7,29 +7,7 @@ class GeoSQLCompiler(BaseGeoSQLCompiler, SQLCompiler):
pass
class SQLInsertCompiler(compiler.SQLInsertCompiler, GeoSQLCompiler):
- def placeholder(self, field, val):
- if field is None:
- # A field value of None means the value is raw.
- return val
- elif hasattr(field, 'get_placeholder'):
- # Some fields (e.g. geo fields) need special munging before
- # they can be inserted.
- ph = field.get_placeholder(val, self.connection)
- if ph == 'NULL':
- # If the placeholder returned is 'NULL', then we need to
- # to remove None from the Query parameters. Specifically,
- # cx_Oracle will assume a CHAR type when a placeholder ('%s')
- # is used for columns of MDSYS.SDO_GEOMETRY. Thus, we use
- # 'NULL' for the value, and remove None from the query params.
- # See also #10888.
- param_idx = self.query.columns.index(field.column)
- params = list(self.query.params)
- params.pop(param_idx)
- self.query.params = tuple(params)
- return ph
- else:
- # Return the common case for the placeholder
- return '%s'
+ pass
class SQLDeleteCompiler(compiler.SQLDeleteCompiler, GeoSQLCompiler):
pass
diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py
index 35a4d9491d..4e42b4cf00 100644
--- a/django/contrib/gis/db/backends/oracle/operations.py
+++ b/django/contrib/gis/db/backends/oracle/operations.py
@@ -288,3 +288,12 @@ class OracleOperations(DatabaseOperations, BaseSpatialOperations):
def spatial_ref_sys(self):
from django.contrib.gis.db.backends.oracle.models import SpatialRefSys
return SpatialRefSys
+
+ def modify_insert_params(self, placeholders, params):
+ """Drop out insert parameters for NULL placeholder. Needed for Oracle Spatial
+ backend due to #10888
+ """
+ # This code doesn't work for bulk insert cases.
+ assert len(placeholders) == 1
+ return [[param for pholder,param
+ in six.moves.zip(placeholders[0], params[0]) if pholder != 'NULL'], ]
diff --git a/django/contrib/gis/db/backends/postgis/creation.py b/django/contrib/gis/db/backends/postgis/creation.py
index 406dc4e487..43ae9a0331 100644
--- a/django/contrib/gis/db/backends/postgis/creation.py
+++ b/django/contrib/gis/db/backends/postgis/creation.py
@@ -1,12 +1,23 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation
+from django.utils.functional import cached_property
+
class PostGISCreation(DatabaseCreation):
geom_index_type = 'GIST'
geom_index_ops = 'GIST_GEOMETRY_OPS'
geom_index_ops_nd = 'GIST_GEOMETRY_OPS_ND'
+ @cached_property
+ def template_postgis(self):
+ template_postgis = getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis')
+ cursor = self.connection.cursor()
+ cursor.execute('SELECT 1 FROM pg_database WHERE datname = %s LIMIT 1;', (template_postgis,))
+ if cursor.fetchone():
+ return template_postgis
+ return None
+
def sql_indexes_for_field(self, model, f, style):
"Return any spatial index creation SQL for the field."
from django.contrib.gis.db.models.fields import GeometryField
@@ -67,5 +78,19 @@ class PostGISCreation(DatabaseCreation):
return output
def sql_table_creation_suffix(self):
- postgis_template = getattr(settings, 'POSTGIS_TEMPLATE', 'template_postgis')
- return ' TEMPLATE %s' % self.connection.ops.quote_name(postgis_template)
+ if self.template_postgis is not None:
+ return ' TEMPLATE %s' % (
+ self.connection.ops.quote_name(self.template_postgis),)
+ return ''
+
+ def _create_test_db(self, verbosity, autoclobber):
+ test_database_name = super(PostGISCreation, self)._create_test_db(verbosity, autoclobber)
+ if self.template_postgis is None:
+ # Connect to the test database in order to create the postgis extension
+ self.connection.close()
+ self.connection.settings_dict["NAME"] = test_database_name
+ cursor = self.connection.cursor()
+ cursor.execute("CREATE EXTENSION postgis")
+ cursor.connection.commit()
+
+ return test_database_name
diff --git a/django/contrib/gis/db/backends/spatialite/base.py b/django/contrib/gis/db/backends/spatialite/base.py
index b447d1d9ff..b3a53820c1 100644
--- a/django/contrib/gis/db/backends/spatialite/base.py
+++ b/django/contrib/gis/db/backends/spatialite/base.py
@@ -36,29 +36,23 @@ class DatabaseWrapper(SQLiteDatabaseWrapper):
self.creation = SpatiaLiteCreation(self)
self.introspection = SpatiaLiteIntrospection(self)
- def _cursor(self):
- if self.connection is None:
- self._sqlite_create_connection()
-
- ## From here on, customized for GeoDjango ##
-
- # Enabling extension loading on the SQLite connection.
- try:
- self.connection.enable_load_extension(True)
- except AttributeError:
- raise ImproperlyConfigured('The pysqlite library does not support C extension loading. '
- 'Both SQLite and pysqlite must be configured to allow '
- 'the loading of extensions to use SpatiaLite.'
- )
-
- # Loading the SpatiaLite library extension on the connection, and returning
- # the created cursor.
- cur = self.connection.cursor(factory=SQLiteCursorWrapper)
- try:
- cur.execute("SELECT load_extension(%s)", (self.spatialite_lib,))
- except Exception as msg:
- raise ImproperlyConfigured('Unable to load the SpatiaLite library extension '
- '"%s" because: %s' % (self.spatialite_lib, msg))
- return cur
- else:
- return self.connection.cursor(factory=SQLiteCursorWrapper)
+ def get_new_connection(self, conn_params):
+ conn = super(DatabaseWrapper, self).get_new_connection(conn_params)
+ # Enabling extension loading on the SQLite connection.
+ try:
+ conn.enable_load_extension(True)
+ except AttributeError:
+ raise ImproperlyConfigured(
+ 'The pysqlite library does not support C extension loading. '
+ 'Both SQLite and pysqlite must be configured to allow '
+ 'the loading of extensions to use SpatiaLite.')
+ # Loading the SpatiaLite library extension on the connection, and returning
+ # the created cursor.
+ cur = conn.cursor(factory=SQLiteCursorWrapper)
+ try:
+ cur.execute("SELECT load_extension(%s)", (self.spatialite_lib,))
+ except Exception as msg:
+ raise ImproperlyConfigured('Unable to load the SpatiaLite library extension '
+ '"%s" because: %s' % (self.spatialite_lib, msg))
+ cur.close()
+ return conn
diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py
index 2ffbd2021b..c89912b2d9 100644
--- a/django/contrib/gis/db/models/query.py
+++ b/django/contrib/gis/db/models/query.py
@@ -760,8 +760,10 @@ class GeoQuerySet(QuerySet):
self.query.add_select_related([field_name])
compiler = self.query.get_compiler(self.db)
compiler.pre_sql_setup()
- rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
- return compiler._field_column(geo_field, rel_table)
+ for (rel_table, rel_col), field in self.query.related_select_cols:
+ if field == geo_field:
+ return compiler._field_column(geo_field, rel_table)
+ raise ValueError("%r not in self.query.related_select_cols" % geo_field)
elif not geo_field in opts.local_fields:
# This geographic field is inherited from another model, so we have to
# use the db table for the _parent_ model instead.
diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py
index cf6a8ad047..81a9941c9e 100644
--- a/django/contrib/gis/db/models/sql/compiler.py
+++ b/django/contrib/gis/db/models/sql/compiler.py
@@ -39,7 +39,7 @@ class GeoSQLCompiler(compiler.SQLCompiler):
if self.query.select:
only_load = self.deferred_to_columns()
# This loop customized for GeoQuery.
- for col, field in zip(self.query.select, self.query.select_fields):
+ for col, field in self.query.select:
if isinstance(col, (list, tuple)):
alias, column = col
table = self.query.alias_map[alias].table_name
@@ -85,7 +85,7 @@ class GeoSQLCompiler(compiler.SQLCompiler):
])
# This loop customized for GeoQuery.
- for (table, col), field in zip(self.query.related_select_cols, self.query.related_select_fields):
+ for (table, col), field in self.query.related_select_cols:
r = self.get_field_select(field, table, col)
if with_aliases and col in col_aliases:
c_alias = 'Col%d' % len(col_aliases)
@@ -101,7 +101,7 @@ class GeoSQLCompiler(compiler.SQLCompiler):
return result
def get_default_columns(self, with_aliases=False, col_aliases=None,
- start_alias=None, opts=None, as_pairs=False, local_only=False):
+ start_alias=None, opts=None, as_pairs=False, from_parent=None):
"""
Computes the default columns for selecting every field in the base
model. Will sometimes be called to pull in related models (e.g. via
@@ -127,7 +127,7 @@ class GeoSQLCompiler(compiler.SQLCompiler):
if start_alias:
seen = {None: start_alias}
for field, model in opts.get_fields_with_model():
- if local_only and model is not None:
+ if from_parent and model is not None and issubclass(from_parent, model):
continue
if start_alias:
try:
diff --git a/django/contrib/gis/geoip/tests.py b/django/contrib/gis/geoip/tests.py
index e53230d9ad..c890c4f4ba 100644
--- a/django/contrib/gis/geoip/tests.py
+++ b/django/contrib/gis/geoip/tests.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import os
diff --git a/django/contrib/gis/geometry/test_data.py b/django/contrib/gis/geometry/test_data.py
index b0f6e1ad57..e13e8589e6 100644
--- a/django/contrib/gis/geometry/test_data.py
+++ b/django/contrib/gis/geometry/test_data.py
@@ -7,13 +7,14 @@ import os
from django.contrib import gis
from django.utils import six
+from django.utils._os import upath
# This global used to store reference geometry data.
GEOMETRIES = None
# Path where reference test data is located.
-TEST_DATA = os.path.join(os.path.dirname(gis.__file__), 'tests', 'data')
+TEST_DATA = os.path.join(os.path.dirname(upath(gis.__file__)), 'tests', 'data')
def tuplize(seq):
diff --git a/django/contrib/gis/geos/mutable_list.py b/django/contrib/gis/geos/mutable_list.py
index 820cdfa5a4..0418282bfe 100644
--- a/django/contrib/gis/geos/mutable_list.py
+++ b/django/contrib/gis/geos/mutable_list.py
@@ -149,27 +149,30 @@ class ListMixin(object):
return self
def __eq__(self, other):
- for i in range(len(self)):
+ olen = len(other)
+ for i in range(olen):
try:
c = self[i] == other[i]
- except IndexError:
- # must be other is shorter
+ except self._IndexError:
+ # self must be shorter
return False
if not c:
return False
- return True
+ return len(self) == olen
def __lt__(self, other):
- slen = len(self)
- for i in range(slen):
+ olen = len(other)
+ for i in range(olen):
try:
c = self[i] < other[i]
- except IndexError:
- # must be other is shorter
- return False
+ except self._IndexError:
+ # self must be shorter
+ return True
if c:
return c
- return slen < len(other)
+ elif other[i] < self[i]:
+ return False
+ return len(self) < olen
### Public list interface Methods ###
## Non-mutating ##
diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py
index 283daa47c0..ec320f94ec 100644
--- a/django/contrib/gis/geos/tests/test_geos.py
+++ b/django/contrib/gis/geos/tests/test_geos.py
@@ -451,6 +451,21 @@ class GEOSTest(unittest.TestCase, TestDataMixin):
self.assertEqual(poly.wkt, Polygon(*tuple(r for r in poly)).wkt)
self.assertEqual(poly.wkt, Polygon(*tuple(LinearRing(r.tuple) for r in poly)).wkt)
+ def test_polygon_comparison(self):
+ p1 = Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)))
+ p2 = Polygon(((0, 0), (0, 1), (1, 0), (0, 0)))
+ self.assertTrue(p1 > p2)
+ self.assertFalse(p1 < p2)
+ self.assertFalse(p2 > p1)
+ self.assertTrue(p2 < p1)
+
+ p3 = Polygon(((0, 0), (0, 1), (1, 1), (2, 0), (0, 0)))
+ p4 = Polygon(((0, 0), (0, 1), (2, 2), (1, 0), (0, 0)))
+ self.assertFalse(p4 < p3)
+ self.assertTrue(p3 < p4)
+ self.assertTrue(p4 > p3)
+ self.assertFalse(p3 > p4)
+
def test_multipolygons(self):
"Testing MultiPolygon objects."
prev = fromstr('POINT (0 0)')
diff --git a/django/contrib/gis/geos/tests/test_mutable_list.py b/django/contrib/gis/geos/tests/test_mutable_list.py
index 675505f0f9..988d8417a2 100644
--- a/django/contrib/gis/geos/tests/test_mutable_list.py
+++ b/django/contrib/gis/geos/tests/test_mutable_list.py
@@ -363,6 +363,7 @@ class ListMixinTest(unittest.TestCase):
pl, ul = self.lists_of_len()
self.assertEqual(pl, ul, 'cmp for equal')
+ self.assertFalse(ul == pl + [2], 'cmp for not equal')
self.assertTrue(pl >= ul, 'cmp for gte self')
self.assertTrue(pl <= ul, 'cmp for lte self')
self.assertTrue(ul >= pl, 'cmp for self gte')
@@ -377,6 +378,14 @@ class ListMixinTest(unittest.TestCase):
self.assertTrue(ul < pl + [2], 'cmp')
self.assertTrue(ul <= pl + [2], 'cmp')
+ # Also works with a custom IndexError
+ ul_longer = ul + [2]
+ ul_longer._IndexError = TypeError
+ ul._IndexError = TypeError
+ self.assertFalse(ul_longer == pl)
+ self.assertFalse(ul == ul_longer)
+ self.assertTrue(ul_longer > ul)
+
pl[1] = 20
self.assertTrue(pl > ul, 'cmp for gt self')
self.assertTrue(ul < pl, 'cmp for self lt')
diff --git a/django/contrib/gis/measure.py b/django/contrib/gis/measure.py
index 6e074be355..e2e6b6bca8 100644
--- a/django/contrib/gis/measure.py
+++ b/django/contrib/gis/measure.py
@@ -151,7 +151,9 @@ class MeasureBase(object):
**{self.STANDARD_UNIT: (self.standard / other)})
else:
raise TypeError('%(class)s must be divided with number or %(class)s' % {"class":pretty_name(self)})
- __div__ = __truediv__ # Python 2 compatibility
+
+ def __div__(self, other): # Python 2 compatibility
+ return type(self).__truediv__(self, other)
def __itruediv__(self, other):
if isinstance(other, NUMERIC_TYPES):
@@ -159,11 +161,15 @@ class MeasureBase(object):
return self
else:
raise TypeError('%(class)s must be divided with number' % {"class":pretty_name(self)})
- __idiv__ = __itruediv__ # Python 2 compatibility
+
+ def __idiv__(self, other): # Python 2 compatibility
+ return type(self).__itruediv__(self, other)
def __bool__(self):
return bool(self.standard)
- __nonzero__ = __bool__ # Python 2 compatibility
+
+ def __nonzero__(self): # Python 2 compatibility
+ return type(self).__bool__(self)
def default_units(self, kwargs):
"""
@@ -314,7 +320,9 @@ class Area(MeasureBase):
**{self.STANDARD_UNIT: (self.standard / other)})
else:
raise TypeError('%(class)s must be divided by a number' % {"class":pretty_name(self)})
- __div__ = __truediv__ # Python 2 compatibility
+
+ def __div__(self, other): # Python 2 compatibility
+ return type(self).__truediv__(self, other)
# Shortcuts
diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py
index f7590fe84a..6b40164422 100644
--- a/django/contrib/gis/tests/geo3d/tests.py
+++ b/django/contrib/gis/tests/geo3d/tests.py
@@ -7,12 +7,13 @@ from django.contrib.gis.db.models import Union, Extent3D
from django.contrib.gis.geos import GEOSGeometry, LineString, Point, Polygon
from django.contrib.gis.utils import LayerMapping, LayerMapError
from django.test import TestCase
+from django.utils._os import upath
from .models import (City3D, Interstate2D, Interstate3D, InterstateProj2D,
InterstateProj3D, Point2D, Point3D, MultiPoint3D, Polygon2D, Polygon3D)
-data_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data'))
+data_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), '..', 'data'))
city_file = os.path.join(data_path, 'cities', 'cities.shp')
vrt_file = os.path.join(data_path, 'test_vrt', 'test_vrt.vrt')
diff --git a/django/contrib/gis/tests/geogapp/tests.py b/django/contrib/gis/tests/geogapp/tests.py
index 2fd3560d0a..a8c607c502 100644
--- a/django/contrib/gis/tests/geogapp/tests.py
+++ b/django/contrib/gis/tests/geogapp/tests.py
@@ -8,6 +8,7 @@ import os
from django.contrib.gis import gdal
from django.contrib.gis.measure import D
from django.test import TestCase
+from django.utils._os import upath
from .models import City, County, Zipcode
@@ -61,7 +62,7 @@ class GeographyTest(TestCase):
from django.contrib.gis.utils import LayerMapping
# Getting the shapefile and mapping dictionary.
- shp_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data'))
+ shp_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), '..', 'data'))
co_shp = os.path.join(shp_path, 'counties', 'counties.shp')
co_mapping = {'name' : 'Name',
'state' : 'State',
diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py
index a976954d25..470e5be216 100644
--- a/django/contrib/gis/tests/layermap/tests.py
+++ b/django/contrib/gis/tests/layermap/tests.py
@@ -13,13 +13,14 @@ from django.db import router
from django.conf import settings
from django.test import TestCase
from django.utils import unittest
+from django.utils._os import upath
from .models import (
City, County, CountyFeat, Interstate, ICity1, ICity2, Invalid, State,
city_mapping, co_mapping, cofeat_mapping, inter_mapping)
-shp_path = os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir, 'data'))
+shp_path = os.path.realpath(os.path.join(os.path.dirname(upath(__file__)), os.pardir, 'data'))
city_shp = os.path.join(shp_path, 'cities', 'cities.shp')
co_shp = os.path.join(shp_path, 'counties', 'counties.shp')
inter_shp = os.path.join(shp_path, 'interstates', 'interstates.shp')
diff --git a/django/contrib/gis/tests/utils.py b/django/contrib/gis/tests/utils.py
index a83ba8a93f..8355b27fd7 100644
--- a/django/contrib/gis/tests/utils.py
+++ b/django/contrib/gis/tests/utils.py
@@ -26,7 +26,7 @@ mysql = _default_db == 'mysql'
spatialite = _default_db == 'spatialite'
HAS_SPATIALREFSYS = True
-if oracle:
+if oracle and 'gis' in settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE']:
from django.contrib.gis.db.backends.oracle.models import SpatialRefSys
elif postgis:
from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
diff --git a/django/contrib/gis/utils/ogrinspect.py b/django/contrib/gis/utils/ogrinspect.py
index 1c870eaa30..08d3b40397 100644
--- a/django/contrib/gis/utils/ogrinspect.py
+++ b/django/contrib/gis/utils/ogrinspect.py
@@ -123,7 +123,7 @@ def _ogrinspect(data_source, model_name, geom_name='geom', layer_key=0, srid=Non
to the given data source. See the `ogrinspect` docstring for more details.
"""
# Getting the DataSource
- if isinstance(data_source, str):
+ if isinstance(data_source, six.string_types):
data_source = DataSource(data_source)
elif isinstance(data_source, DataSource):
pass
diff --git a/django/contrib/humanize/tests.py b/django/contrib/humanize/tests.py
index a0f13d3ee9..c648f544d7 100644
--- a/django/contrib/humanize/tests.py
+++ b/django/contrib/humanize/tests.py
@@ -1,6 +1,12 @@
from __future__ import unicode_literals
import datetime
+try:
+ import pytz
+except ImportError:
+ pytz = None
+
+from django.conf import settings
from django.contrib.humanize.templatetags import humanize
from django.template import Template, Context, defaultfilters
from django.test import TestCase
@@ -10,6 +16,7 @@ from django.utils.timezone import utc
from django.utils import translation
from django.utils.translation import ugettext as _
from django.utils import tzinfo
+from django.utils.unittest import skipIf
# Mock out datetime in some tests so they don't fail occasionally when they
@@ -141,14 +148,16 @@ class HumanizeTests(TestCase):
# As 24h of difference they will never be the same
self.assertNotEqual(naturalday_one, naturalday_two)
+ @skipIf(settings.TIME_ZONE != "America/Chicago" and pytz is None,
+ "this test requires pytz when a non-default time zone is set")
def test_naturalday_uses_localtime(self):
# Regression for #18504
- # This is 2012-03-08HT19:30:00-06:00 in Ameria/Chicago
+ # This is 2012-03-08HT19:30:00-06:00 in America/Chicago
dt = datetime.datetime(2012, 3, 9, 1, 30, tzinfo=utc)
orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime
try:
- with override_settings(USE_TZ=True):
+ with override_settings(TIME_ZONE="America/Chicago", USE_TZ=True):
self.humanize_tester([dt], ['yesterday'], 'naturalday')
finally:
humanize.datetime = orig_humanize_datetime
diff --git a/django/contrib/localflavor/ar/forms.py b/django/contrib/localflavor/ar/forms.py
index dc4235f9dd..cc6c833de0 100644
--- a/django/contrib/localflavor/ar/forms.py
+++ b/django/contrib/localflavor/ar/forms.py
@@ -24,7 +24,9 @@ class ARPostalCodeField(RegexField):
"""
A field that accepts a 'classic' NNNN Postal Code or a CPA.
- See http://www.correoargentino.com.ar/consulta_cpa/home.php
+ See:
+ http://www.correoargentino.com.ar/cpa/que_es
+ http://www.correoargentino.com.ar/cpa/como_escribirlo
"""
default_error_messages = {
'invalid': _("Enter a postal code in the format NNNN or ANNNNAAA."),
@@ -120,8 +122,7 @@ class ARCUITField(RegexField):
return str(result)
def _format(self, cuit, check_digit=None):
- if check_digit == None:
+ if check_digit is None:
check_digit = cuit[-1]
cuit = cuit[:-1]
return '%s-%s-%s' % (cuit[:2], cuit[2:], check_digit)
-
diff --git a/django/contrib/redirects/tests.py b/django/contrib/redirects/tests.py
new file mode 100644
index 0000000000..11ffc7b748
--- /dev/null
+++ b/django/contrib/redirects/tests.py
@@ -0,0 +1,42 @@
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.test import TestCase
+from django.test.utils import override_settings
+from django.utils import six
+
+from .models import Redirect
+
+
+@override_settings(
+ SITE_ID=1,
+ APPEND_SLASH=True,
+ MIDDLEWARE_CLASSES=list(settings.MIDDLEWARE_CLASSES) +
+ ['django.contrib.redirects.middleware.RedirectFallbackMiddleware'],
+)
+class RedirectTests(TestCase):
+
+ def setUp(self):
+ self.site = Site.objects.get(pk=settings.SITE_ID)
+
+ def test_model(self):
+ r1 = Redirect.objects.create(
+ site=self.site, old_path='/initial', new_path='/new_target')
+ self.assertEqual(six.text_type(r1), "/initial ---> /new_target")
+
+ def test_redirect_middleware(self):
+ r1 = Redirect.objects.create(
+ site=self.site, old_path='/initial', new_path='/new_target')
+ response = self.client.get('/initial')
+ self.assertRedirects(response,
+ '/new_target', status_code=301, target_status_code=404)
+ # Works also with trailing slash
+ response = self.client.get('/initial/')
+ self.assertRedirects(response,
+ '/new_target', status_code=301, target_status_code=404)
+
+ def test_response_gone(self):
+ """When the redirect target is '', return a 410"""
+ r1 = Redirect.objects.create(
+ site=self.site, old_path='/initial', new_path='')
+ response = self.client.get('/initial')
+ self.assertEqual(response.status_code, 410)
diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py
index c8393f23c6..f79a264500 100644
--- a/django/contrib/sessions/backends/base.py
+++ b/django/contrib/sessions/backends/base.py
@@ -1,12 +1,12 @@
from __future__ import unicode_literals
import base64
-import time
from datetime import datetime, timedelta
try:
from django.utils.six.moves import cPickle as pickle
except ImportError:
import pickle
+import string
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
@@ -16,6 +16,10 @@ from django.utils.crypto import salted_hmac
from django.utils import timezone
from django.utils.encoding import force_bytes
+# session_key should not be case sensitive because some backends can store it
+# on case insensitive file systems.
+VALID_KEY_CHARS = string.ascii_lowercase + string.digits
+
class CreateError(Exception):
"""
Used internally as a consistent exception type to catch from save (see the
@@ -133,12 +137,8 @@ class SessionBase(object):
def _get_new_session_key(self):
"Returns session key that isn't being used."
- # Todo: move to 0-9a-z charset in 1.5
- hex_chars = '1234567890abcdef'
- # session_key should not be case sensitive because some backends
- # can store it on case insensitive file systems.
while True:
- session_key = get_random_string(32, hex_chars)
+ session_key = get_random_string(32, VALID_KEY_CHARS)
if not self.exists(session_key):
break
return session_key
@@ -170,24 +170,52 @@ class SessionBase(object):
_session = property(_get_session)
- def get_expiry_age(self):
- """Get the number of seconds until the session expires."""
- expiry = self.get('_session_expiry')
+ def get_expiry_age(self, **kwargs):
+ """Get the number of seconds until the session expires.
+
+ Optionally, this function accepts `modification` and `expiry` keyword
+ arguments specifying the modification and expiry of the session.
+ """
+ try:
+ modification = kwargs['modification']
+ except KeyError:
+ modification = timezone.now()
+ # Make the difference between "expiry=None passed in kwargs" and
+ # "expiry not passed in kwargs", in order to guarantee not to trigger
+ # self.load() when expiry is provided.
+ try:
+ expiry = kwargs['expiry']
+ except KeyError:
+ expiry = self.get('_session_expiry')
+
if not expiry: # Checks both None and 0 cases
return settings.SESSION_COOKIE_AGE
if not isinstance(expiry, datetime):
return expiry
- delta = expiry - timezone.now()
+ delta = expiry - modification
return delta.days * 86400 + delta.seconds
- def get_expiry_date(self):
- """Get session the expiry date (as a datetime object)."""
- expiry = self.get('_session_expiry')
+ def get_expiry_date(self, **kwargs):
+ """Get session the expiry date (as a datetime object).
+
+ Optionally, this function accepts `modification` and `expiry` keyword
+ arguments specifying the modification and expiry of the session.
+ """
+ try:
+ modification = kwargs['modification']
+ except KeyError:
+ modification = timezone.now()
+ # Same comment as in get_expiry_age
+ try:
+ expiry = kwargs['expiry']
+ except KeyError:
+ expiry = self.get('_session_expiry')
+
if isinstance(expiry, datetime):
return expiry
if not expiry: # Checks both None and 0 cases
expiry = settings.SESSION_COOKIE_AGE
- return timezone.now() + timedelta(seconds=expiry)
+ return modification + timedelta(seconds=expiry)
def set_expiry(self, value):
"""
@@ -281,3 +309,14 @@ class SessionBase(object):
Loads the session data and returns a dictionary.
"""
raise NotImplementedError
+
+ @classmethod
+ def clear_expired(cls):
+ """
+ Remove expired sessions from the session store.
+
+ If this operation isn't possible on a given backend, it should raise
+ NotImplementedError. If it isn't necessary, because the backend has
+ a built-in expiration mechanism, it should be a no-op.
+ """
+ raise NotImplementedError
diff --git a/django/contrib/sessions/backends/cache.py b/django/contrib/sessions/backends/cache.py
index b66123b915..596042fcb3 100644
--- a/django/contrib/sessions/backends/cache.py
+++ b/django/contrib/sessions/backends/cache.py
@@ -1,5 +1,6 @@
+from django.conf import settings
from django.contrib.sessions.backends.base import SessionBase, CreateError
-from django.core.cache import cache
+from django.core.cache import get_cache
from django.utils.six.moves import xrange
KEY_PREFIX = "django.contrib.sessions.cache"
@@ -10,7 +11,7 @@ class SessionStore(SessionBase):
A cache-based session store.
"""
def __init__(self, session_key=None):
- self._cache = cache
+ self._cache = get_cache(settings.SESSION_CACHE_ALIAS)
super(SessionStore, self).__init__(session_key)
@property
@@ -43,7 +44,9 @@ class SessionStore(SessionBase):
continue
self.modified = True
return
- raise RuntimeError("Unable to create a new session key.")
+ raise RuntimeError(
+ "Unable to create a new session key. "
+ "It is likely that the cache is unavailable.")
def save(self, must_create=False):
if must_create:
@@ -65,3 +68,7 @@ class SessionStore(SessionBase):
return
session_key = self.session_key
self._cache.delete(KEY_PREFIX + session_key)
+
+ @classmethod
+ def clear_expired(cls):
+ pass
diff --git a/django/contrib/sessions/backends/cached_db.py b/django/contrib/sessions/backends/cached_db.py
index ff6076df77..31c6fbfce3 100644
--- a/django/contrib/sessions/backends/cached_db.py
+++ b/django/contrib/sessions/backends/cached_db.py
@@ -2,9 +2,10 @@
Cached, database-backed sessions.
"""
-from django.conf import settings
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
KEY_PREFIX = "django.contrib.sessions.cached_db"
@@ -28,9 +29,21 @@ class SessionStore(DBStore):
# Some backends (e.g. memcache) raise an exception on invalid
# cache keys. If this happens, reset the session. See #17810.
data = None
+
if data is None:
- data = super(SessionStore, self).load()
- cache.set(self.cache_key, data, settings.SESSION_COOKIE_AGE)
+ # Duplicate DBStore.load, because we need to keep track
+ # of the expiry date to set it properly in the cache.
+ try:
+ s = Session.objects.get(
+ session_key=self.session_key,
+ expire_date__gt=timezone.now()
+ )
+ data = self.decode(s.session_data)
+ cache.set(self.cache_key, data,
+ self.get_expiry_age(expiry=s.expire_date))
+ except (Session.DoesNotExist, SuspiciousOperation):
+ self.create()
+ data = {}
return data
def exists(self, session_key):
@@ -40,7 +53,7 @@ class SessionStore(DBStore):
def save(self, must_create=False):
super(SessionStore, self).save(must_create)
- cache.set(self.cache_key, self._session, settings.SESSION_COOKIE_AGE)
+ cache.set(self.cache_key, self._session, self.get_expiry_age())
def delete(self, session_key=None):
super(SessionStore, self).delete(session_key)
@@ -58,3 +71,7 @@ class SessionStore(DBStore):
self.clear()
self.delete(self.session_key)
self.create()
+
+
+# At bottom to avoid circular import
+from django.contrib.sessions.models import Session
diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py
index babdb72c27..47e89b66e5 100644
--- a/django/contrib/sessions/backends/db.py
+++ b/django/contrib/sessions/backends/db.py
@@ -14,7 +14,7 @@ class SessionStore(SessionBase):
def load(self):
try:
s = Session.objects.get(
- session_key = self.session_key,
+ session_key=self.session_key,
expire_date__gt=timezone.now()
)
return self.decode(s.session_data)
@@ -71,6 +71,11 @@ class SessionStore(SessionBase):
except Session.DoesNotExist:
pass
+ @classmethod
+ def clear_expired(cls):
+ Session.objects.filter(expire_date__lt=timezone.now()).delete()
+ transaction.commit_unless_managed()
+
# At bottom to avoid circular import
from django.contrib.sessions.models import Session
diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py
index 20ac2c2087..7d933c678a 100644
--- a/django/contrib/sessions/backends/file.py
+++ b/django/contrib/sessions/backends/file.py
@@ -1,32 +1,40 @@
+import datetime
import errno
import os
import tempfile
from django.conf import settings
-from django.contrib.sessions.backends.base import SessionBase, CreateError
+from django.contrib.sessions.backends.base import SessionBase, CreateError, VALID_KEY_CHARS
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
-
+from django.utils import timezone
class SessionStore(SessionBase):
"""
Implements a file based session store.
"""
def __init__(self, session_key=None):
- self.storage_path = getattr(settings, "SESSION_FILE_PATH", None)
- if not self.storage_path:
- self.storage_path = tempfile.gettempdir()
-
- # Make sure the storage path is valid.
- if not os.path.isdir(self.storage_path):
- raise ImproperlyConfigured(
- "The session storage path %r doesn't exist. Please set your"
- " SESSION_FILE_PATH setting to an existing directory in which"
- " Django can store session data." % self.storage_path)
-
+ self.storage_path = type(self)._get_storage_path()
self.file_prefix = settings.SESSION_COOKIE_NAME
super(SessionStore, self).__init__(session_key)
- VALID_KEY_CHARS = set("abcdef0123456789")
+ @classmethod
+ def _get_storage_path(cls):
+ try:
+ return cls._storage_path
+ except AttributeError:
+ storage_path = getattr(settings, "SESSION_FILE_PATH", None)
+ if not storage_path:
+ storage_path = tempfile.gettempdir()
+
+ # Make sure the storage path is valid.
+ if not os.path.isdir(storage_path):
+ raise ImproperlyConfigured(
+ "The session storage path %r doesn't exist. Please set your"
+ " SESSION_FILE_PATH setting to an existing directory in which"
+ " Django can store session data." % storage_path)
+
+ cls._storage_path = storage_path
+ return storage_path
def _key_to_file(self, session_key=None):
"""
@@ -38,12 +46,24 @@ class SessionStore(SessionBase):
# Make sure we're not vulnerable to directory traversal. Session keys
# should always be md5s, so they should never contain directory
# components.
- if not set(session_key).issubset(self.VALID_KEY_CHARS):
+ if not set(session_key).issubset(set(VALID_KEY_CHARS)):
raise SuspiciousOperation(
"Invalid characters in session key")
return os.path.join(self.storage_path, self.file_prefix + session_key)
+ def _last_modification(self):
+ """
+ Return the modification time of the file storing the session's content.
+ """
+ modification = os.stat(self._key_to_file()).st_mtime
+ if settings.USE_TZ:
+ modification = datetime.datetime.utcfromtimestamp(modification)
+ modification = modification.replace(tzinfo=timezone.utc)
+ else:
+ modification = datetime.datetime.fromtimestamp(modification)
+ return modification
+
def load(self):
session_data = {}
try:
@@ -56,6 +76,15 @@ class SessionStore(SessionBase):
session_data = self.decode(file_data)
except (EOFError, SuspiciousOperation):
self.create()
+
+ # Remove expired sessions.
+ expiry_age = self.get_expiry_age(
+ modification=self._last_modification(),
+ expiry=session_data.get('_session_expiry'))
+ if expiry_age < 0:
+ session_data = {}
+ self.delete()
+ self.create()
except IOError:
self.create()
return session_data
@@ -142,3 +171,19 @@ class SessionStore(SessionBase):
def clean(self):
pass
+
+ @classmethod
+ def clear_expired(cls):
+ storage_path = cls._get_storage_path()
+ file_prefix = settings.SESSION_COOKIE_NAME
+
+ for session_file in os.listdir(storage_path):
+ if not session_file.startswith(file_prefix):
+ continue
+ session_key = session_file[len(file_prefix):]
+ session = cls(session_key)
+ # When an expired session is loaded, its file is removed, and a
+ # new file is immediately created. Prevent this by disabling
+ # the create() method.
+ session.create = lambda: None
+ session.load()
diff --git a/django/contrib/sessions/backends/signed_cookies.py b/django/contrib/sessions/backends/signed_cookies.py
index 41ba7af634..c2b7a3123f 100644
--- a/django/contrib/sessions/backends/signed_cookies.py
+++ b/django/contrib/sessions/backends/signed_cookies.py
@@ -32,6 +32,7 @@ class SessionStore(SessionBase):
try:
return signing.loads(self.session_key,
serializer=PickleSerializer,
+ # This doesn't handle non-default expiry dates, see #19201
max_age=settings.SESSION_COOKIE_AGE,
salt='django.contrib.sessions.backends.signed_cookies')
except (signing.BadSignature, ValueError):
@@ -91,3 +92,7 @@ class SessionStore(SessionBase):
return signing.dumps(session_cache, compress=True,
salt='django.contrib.sessions.backends.signed_cookies',
serializer=PickleSerializer)
+
+ @classmethod
+ def clear_expired(cls):
+ pass
diff --git a/tests/modeltests/pagination/__init__.py b/django/contrib/sessions/management/__init__.py
index e69de29bb2..e69de29bb2 100644
--- a/tests/modeltests/pagination/__init__.py
+++ b/django/contrib/sessions/management/__init__.py
diff --git a/tests/regressiontests/pagination_regress/__init__.py b/django/contrib/sessions/management/commands/__init__.py
index e69de29bb2..e69de29bb2 100644
--- a/tests/regressiontests/pagination_regress/__init__.py
+++ b/django/contrib/sessions/management/commands/__init__.py
diff --git a/django/contrib/sessions/management/commands/clearsessions.py b/django/contrib/sessions/management/commands/clearsessions.py
new file mode 100644
index 0000000000..8eb23dfee0
--- /dev/null
+++ b/django/contrib/sessions/management/commands/clearsessions.py
@@ -0,0 +1,15 @@
+from django.conf import settings
+from django.core.management.base import NoArgsCommand
+from django.utils.importlib import import_module
+
+
+class Command(NoArgsCommand):
+ help = "Can be run as a cronjob or directly to clean out expired sessions (only with the database backend at the moment)."
+
+ def handle_noargs(self, **options):
+ engine = import_module(settings.SESSION_ENGINE)
+ try:
+ engine.SessionStore.clear_expired()
+ except NotImplementedError:
+ self.stderr.write("Session engine '%s' doesn't support clearing "
+ "expired sessions.\n" % settings.SESSION_ENGINE)
diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py
index fc2d8753d7..da79ac9de6 100644
--- a/django/contrib/sessions/tests.py
+++ b/django/contrib/sessions/tests.py
@@ -1,4 +1,5 @@
from datetime import timedelta
+import os
import shutil
import string
import tempfile
@@ -12,7 +13,8 @@ from django.contrib.sessions.backends.file import SessionStore as FileSession
from django.contrib.sessions.backends.signed_cookies import SessionStore as CookieSession
from django.contrib.sessions.models import Session
from django.contrib.sessions.middleware import SessionMiddleware
-from django.core.cache import DEFAULT_CACHE_ALIAS
+from django.core.cache import get_cache
+from django.core import management
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
from django.http import HttpResponse
from django.test import TestCase, RequestFactory
@@ -83,7 +85,7 @@ class SessionTestsMixin(object):
self.session['some key'] = 1
self.session.modified = False
self.session.accessed = False
- self.assertTrue('some key' in self.session)
+ self.assertIn('some key', self.session)
self.assertTrue(self.session.accessed)
self.assertFalse(self.session.modified)
@@ -134,8 +136,8 @@ class SessionTestsMixin(object):
self.assertTrue(self.session.modified)
def test_save(self):
- if (hasattr(self.session, '_cache') and
- 'DummyCache' in settings.CACHES[DEFAULT_CACHE_ALIAS]['BACKEND']):
+ if (hasattr(self.session, '_cache') and'DummyCache' in
+ settings.CACHES[settings.SESSION_CACHE_ALIAS]['BACKEND']):
raise unittest.SkipTest("Session saving tests require a real cache backend")
self.session.save()
self.assertTrue(self.session.exists(self.session.session_key))
@@ -197,31 +199,43 @@ class SessionTestsMixin(object):
self.assertEqual(self.session.get_expiry_age(), settings.SESSION_COOKIE_AGE)
def test_custom_expiry_seconds(self):
- # Using seconds
+ modification = timezone.now()
+
self.session.set_expiry(10)
- delta = self.session.get_expiry_date() - timezone.now()
- self.assertTrue(delta.seconds in (9, 10))
- age = self.session.get_expiry_age()
- self.assertTrue(age in (9, 10))
+ date = self.session.get_expiry_date(modification=modification)
+ self.assertEqual(date, modification + timedelta(seconds=10))
+
+ age = self.session.get_expiry_age(modification=modification)
+ self.assertEqual(age, 10)
def test_custom_expiry_timedelta(self):
- # Using timedelta
- self.session.set_expiry(timedelta(seconds=10))
- delta = self.session.get_expiry_date() - timezone.now()
- self.assertTrue(delta.seconds in (9, 10))
+ modification = timezone.now()
+
+ # Mock timezone.now, because set_expiry calls it on this code path.
+ original_now = timezone.now
+ try:
+ timezone.now = lambda: modification
+ self.session.set_expiry(timedelta(seconds=10))
+ finally:
+ timezone.now = original_now
- age = self.session.get_expiry_age()
- self.assertTrue(age in (9, 10))
+ date = self.session.get_expiry_date(modification=modification)
+ self.assertEqual(date, modification + timedelta(seconds=10))
+
+ age = self.session.get_expiry_age(modification=modification)
+ self.assertEqual(age, 10)
def test_custom_expiry_datetime(self):
- # Using fixed datetime
- self.session.set_expiry(timezone.now() + timedelta(seconds=10))
- delta = self.session.get_expiry_date() - timezone.now()
- self.assertTrue(delta.seconds in (9, 10))
+ modification = timezone.now()
+
+ self.session.set_expiry(modification + timedelta(seconds=10))
+
+ date = self.session.get_expiry_date(modification=modification)
+ self.assertEqual(date, modification + timedelta(seconds=10))
- age = self.session.get_expiry_age()
- self.assertTrue(age in (9, 10))
+ age = self.session.get_expiry_age(modification=modification)
+ self.assertEqual(age, 10)
def test_custom_expiry_reset(self):
self.session.set_expiry(None)
@@ -258,6 +272,23 @@ class SessionTestsMixin(object):
encoded = self.session.encode(data)
self.assertEqual(self.session.decode(encoded), data)
+ def test_actual_expiry(self):
+ # Regression test for #19200
+ old_session_key = None
+ new_session_key = None
+ try:
+ self.session['foo'] = 'bar'
+ self.session.set_expiry(-timedelta(seconds=10))
+ self.session.save()
+ old_session_key = self.session.session_key
+ # With an expiry date in the past, the session expires instantly.
+ new_session = self.backend(self.session.session_key)
+ new_session_key = new_session.session_key
+ self.assertNotIn('foo', new_session)
+ finally:
+ self.session.delete(old_session_key)
+ self.session.delete(new_session_key)
+
class DatabaseSessionTests(SessionTestsMixin, TestCase):
@@ -290,6 +321,30 @@ class DatabaseSessionTests(SessionTestsMixin, TestCase):
del self.session._session_cache
self.assertEqual(self.session['y'], 2)
+ @override_settings(SESSION_ENGINE="django.contrib.sessions.backends.db")
+ def test_clearsessions_command(self):
+ """
+ Test clearsessions command for clearing expired sessions.
+ """
+ self.assertEqual(0, Session.objects.count())
+
+ # One object in the future
+ self.session['foo'] = 'bar'
+ self.session.set_expiry(3600)
+ self.session.save()
+
+ # One object in the past
+ other_session = self.backend()
+ other_session['foo'] = 'bar'
+ other_session.set_expiry(-3600)
+ other_session.save()
+
+ # Two sessions are in the database before clearsessions...
+ self.assertEqual(2, Session.objects.count())
+ management.call_command('clearsessions')
+ # ... and one is deleted.
+ self.assertEqual(1, Session.objects.count())
+
@override_settings(USE_TZ=True)
class DatabaseSessionWithTimeZoneTests(DatabaseSessionTests):
@@ -300,7 +355,8 @@ class CacheDBSessionTests(SessionTestsMixin, TestCase):
backend = CacheDBSession
- @unittest.skipIf('DummyCache' in settings.CACHES[DEFAULT_CACHE_ALIAS]['BACKEND'],
+ @unittest.skipIf('DummyCache' in
+ settings.CACHES[settings.SESSION_CACHE_ALIAS]['BACKEND'],
"Session saving tests require a real cache backend")
def test_exists_searches_cache_first(self):
self.session.save()
@@ -326,19 +382,23 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
backend = FileSession
def setUp(self):
- super(FileSessionTests, self).setUp()
# Do file session tests in an isolated directory, and kill it after we're done.
self.original_session_file_path = settings.SESSION_FILE_PATH
self.temp_session_store = settings.SESSION_FILE_PATH = tempfile.mkdtemp()
+ # Reset the file session backend's internal caches
+ if hasattr(self.backend, '_storage_path'):
+ del self.backend._storage_path
+ super(FileSessionTests, self).setUp()
def tearDown(self):
+ super(FileSessionTests, self).tearDown()
settings.SESSION_FILE_PATH = self.original_session_file_path
shutil.rmtree(self.temp_session_store)
- super(FileSessionTests, self).tearDown()
@override_settings(
SESSION_FILE_PATH="/if/this/directory/exists/you/have/a/weird/computer")
def test_configuration_check(self):
+ del self.backend._storage_path
# Make sure the file backend checks for a good storage dir
self.assertRaises(ImproperlyConfigured, self.backend)
@@ -352,6 +412,37 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
self.assertRaises(SuspiciousOperation,
self.backend("a/b/c").load)
+ @override_settings(SESSION_ENGINE="django.contrib.sessions.backends.file")
+ def test_clearsessions_command(self):
+ """
+ Test clearsessions command for clearing expired sessions.
+ """
+ storage_path = self.backend._get_storage_path()
+ file_prefix = settings.SESSION_COOKIE_NAME
+
+ def count_sessions():
+ return len([session_file for session_file in os.listdir(storage_path)
+ if session_file.startswith(file_prefix)])
+
+ self.assertEqual(0, count_sessions())
+
+ # One object in the future
+ self.session['foo'] = 'bar'
+ self.session.set_expiry(3600)
+ self.session.save()
+
+ # One object in the past
+ other_session = self.backend()
+ other_session['foo'] = 'bar'
+ other_session.set_expiry(-3600)
+ other_session.save()
+
+ # Two sessions are in the filesystem before clearsessions...
+ self.assertEqual(2, count_sessions())
+ management.call_command('clearsessions')
+ # ... and one is deleted.
+ self.assertEqual(1, count_sessions())
+
class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
@@ -364,6 +455,23 @@ class CacheSessionTests(SessionTestsMixin, unittest.TestCase):
self.session._session_key = (string.ascii_letters + string.digits) * 20
self.assertEqual(self.session.load(), {})
+ def test_default_cache(self):
+ self.session.save()
+ self.assertNotEqual(get_cache('default').get(self.session.cache_key), None)
+
+ @override_settings(CACHES={
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
+ },
+ 'sessions': {
+ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
+ },
+ }, SESSION_CACHE_ALIAS='sessions')
+ def test_non_default_cache(self):
+ self.session.save()
+ self.assertEqual(get_cache('default').get(self.session.cache_key), None)
+ self.assertNotEqual(get_cache('sessions').get(self.session.cache_key), None)
+
class SessionMiddlewareTests(unittest.TestCase):
@@ -452,3 +560,8 @@ class CookieSessionTests(SessionTestsMixin, TestCase):
testing for this behavior is meaningless.
"""
pass
+
+ @unittest.expectedFailure
+ def test_actual_expiry(self):
+ # The cookie backend doesn't handle non-default expiry dates, see #19201
+ super(CookieSessionTests, self).test_actual_expiry()
diff --git a/django/contrib/sitemaps/tests/http.py b/django/contrib/sitemaps/tests/http.py
index 99042fef03..4a1cf66b17 100644
--- a/django/contrib/sitemaps/tests/http.py
+++ b/django/contrib/sitemaps/tests/http.py
@@ -11,6 +11,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.test.utils import override_settings
from django.utils.unittest import skipUnless
from django.utils.formats import localize
+from django.utils._os import upath
from django.utils.translation import activate, deactivate
from .base import SitemapTestsBase
@@ -29,7 +30,7 @@ class HTTPSitemapTests(SitemapTestsBase):
self.assertXMLEqual(response.content.decode('utf-8'), expected_content)
@override_settings(
- TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),)
+ TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'templates'),)
)
def test_simple_sitemap_custom_index(self):
"A simple sitemap index can be rendered with a custom template"
@@ -64,7 +65,7 @@ class HTTPSitemapTests(SitemapTestsBase):
self.assertXMLEqual(response.content.decode('utf-8'), expected_content)
@override_settings(
- TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),)
+ TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'templates'),)
)
def test_simple_custom_sitemap(self):
"A simple sitemap can be rendered with a custom template"
diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py
index 9691b7849d..7e87a89f5b 100644
--- a/django/contrib/staticfiles/storage.py
+++ b/django/contrib/staticfiles/storage.py
@@ -19,6 +19,7 @@ from django.utils.datastructures import SortedDict
from django.utils.encoding import force_bytes, force_text
from django.utils.functional import LazyObject
from django.utils.importlib import import_module
+from django.utils._os import upath
from django.contrib.staticfiles.utils import check_settings, matches_patterns
@@ -296,7 +297,7 @@ class AppStaticStorage(FileSystemStorage):
"""
# app is the actual app module
mod = import_module(app)
- mod_path = os.path.dirname(mod.__file__)
+ mod_path = os.path.dirname(upath(mod.__file__))
location = os.path.join(mod_path, self.source_dir)
super(AppStaticStorage, self).__init__(location, *args, **kwargs)
diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py
index f496c35e2b..562bcc21bb 100644
--- a/django/core/cache/__init__.py
+++ b/django/core/cache/__init__.py
@@ -93,8 +93,6 @@ def parse_backend_conf(backend, **kwargs):
raise InvalidCacheBackendError("Could not find backend '%s'" % backend)
location = kwargs.pop('LOCATION', '')
return backend, location, kwargs
- raise InvalidCacheBackendError(
- "Couldn't find a cache backend named '%s'" % backend)
def get_cache(backend, **kwargs):
"""
@@ -136,11 +134,9 @@ def get_cache(backend, **kwargs):
"Could not find backend '%s': %s" % (backend, e))
cache = backend_cls(location, params)
# Some caches -- python-memcached in particular -- need to do a cleanup at the
- # end of a request cycle. If the cache provides a close() method, wire it up
- # here.
- if hasattr(cache, 'close'):
- signals.request_finished.connect(cache.close)
+ # end of a request cycle. If not implemented in a particular backend
+ # cache.close is a no-op
+ signals.request_finished.connect(cache.close)
return cache
cache = get_cache(DEFAULT_CACHE_ALIAS)
-
diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py
index 06e8952bfb..7234d3c4db 100644
--- a/django/core/cache/backends/base.py
+++ b/django/core/cache/backends/base.py
@@ -6,15 +6,19 @@ import warnings
from django.core.exceptions import ImproperlyConfigured, DjangoRuntimeWarning
from django.utils.importlib import import_module
+
class InvalidCacheBackendError(ImproperlyConfigured):
pass
+
class CacheKeyWarning(DjangoRuntimeWarning):
pass
+
# Memcached does not accept keys longer than this.
MEMCACHE_MAX_KEY_LENGTH = 250
+
def default_key_func(key, key_prefix, version):
"""
Default function to generate keys.
@@ -23,7 +27,8 @@ def default_key_func(key, key_prefix, version):
the `key_prefix'. KEY_FUNCTION can be used to specify an alternate
function with custom key making behavior.
"""
- return ':'.join([key_prefix, str(version), key])
+ return '%s:%s:%s' % (key_prefix, version, key)
+
def get_key_func(key_func):
"""
@@ -40,6 +45,7 @@ def get_key_func(key_func):
return getattr(key_func_module, key_func_name)
return default_key_func
+
class BaseCache(object):
def __init__(self, params):
timeout = params.get('timeout', params.get('TIMEOUT', 300))
@@ -221,3 +227,7 @@ class BaseCache(object):
the new version.
"""
return self.incr_version(key, -delta, version)
+
+ def close(self, **kwargs):
+ """Close the cache connection"""
+ pass
diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py
index 348b03f733..c93bc90b18 100644
--- a/django/core/cache/backends/db.py
+++ b/django/core/cache/backends/db.py
@@ -11,7 +11,7 @@ except ImportError:
from django.conf import settings
from django.core.cache.backends.base import BaseCache
from django.db import connections, router, transaction, DatabaseError
-from django.utils import timezone
+from django.utils import timezone, six
from django.utils.encoding import force_bytes
@@ -104,7 +104,11 @@ class DatabaseCache(BaseDatabaseCache):
if num > self._max_entries:
self._cull(db, cursor, now)
pickled = pickle.dumps(value, pickle.HIGHEST_PROTOCOL)
- encoded = base64.b64encode(pickled).strip()
+ b64encoded = base64.b64encode(pickled)
+ # The DB column is expecting a string, so make sure the value is a
+ # string, not bytes. Refs #19274.
+ if six.PY3:
+ b64encoded = b64encoded.decode('latin1')
cursor.execute("SELECT cache_key, expires FROM %s "
"WHERE cache_key = %%s" % table, [key])
try:
@@ -113,11 +117,11 @@ class DatabaseCache(BaseDatabaseCache):
(mode == 'add' and result[1] < now)):
cursor.execute("UPDATE %s SET value = %%s, expires = %%s "
"WHERE cache_key = %%s" % table,
- [encoded, connections[db].ops.value_to_db_datetime(exp), key])
+ [b64encoded, connections[db].ops.value_to_db_datetime(exp), key])
else:
cursor.execute("INSERT INTO %s (cache_key, value, expires) "
"VALUES (%%s, %%s, %%s)" % table,
- [key, encoded, connections[db].ops.value_to_db_datetime(exp)])
+ [key, b64encoded, connections[db].ops.value_to_db_datetime(exp)])
except DatabaseError:
# To be threadsafe, updates/inserts are allowed to fail silently
transaction.rollback_unless_managed(using=db)
diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py
index 9bb47c8344..2c3f198847 100644
--- a/django/core/cache/backends/memcached.py
+++ b/django/core/cache/backends/memcached.py
@@ -90,6 +90,9 @@ class BaseMemcachedCache(BaseCache):
def incr(self, key, delta=1, version=None):
key = self.make_key(key, version=version)
+ # memcached doesn't support a negative delta
+ if delta < 0:
+ return self._cache.decr(key, -delta)
try:
val = self._cache.incr(key, delta)
@@ -105,6 +108,9 @@ class BaseMemcachedCache(BaseCache):
def decr(self, key, delta=1, version=None):
key = self.make_key(key, version=version)
+ # memcached doesn't support a negative delta
+ if delta < 0:
+ return self._cache.incr(key, -delta)
try:
val = self._cache.decr(key, delta)
diff --git a/django/core/files/base.py b/django/core/files/base.py
index b81e180292..2d10100019 100644
--- a/django/core/files/base.py
+++ b/django/core/files/base.py
@@ -6,7 +6,7 @@ from io import BytesIO, StringIO, UnsupportedOperation
from django.utils.encoding import smart_text
from django.core.files.utils import FileProxyMixin
from django.utils import six
-from django.utils.encoding import python_2_unicode_compatible
+from django.utils.encoding import force_bytes, python_2_unicode_compatible
@python_2_unicode_compatible
class File(FileProxyMixin):
@@ -28,7 +28,9 @@ class File(FileProxyMixin):
def __bool__(self):
return bool(self.name)
- __nonzero__ = __bool__ # Python 2
+
+ def __nonzero__(self): # Python 2 compatibility
+ return type(self).__bool__(self)
def __len__(self):
return self.size
@@ -132,8 +134,11 @@ class ContentFile(File):
A File-like object that takes just raw content, rather than an actual file.
"""
def __init__(self, content, name=None):
- content = content or b''
- stream_class = StringIO if isinstance(content, six.text_type) else BytesIO
+ if six.PY3:
+ stream_class = StringIO if isinstance(content, six.text_type) else BytesIO
+ else:
+ stream_class = BytesIO
+ content = force_bytes(content)
super(ContentFile, self).__init__(stream_class(content), name=name)
self.size = len(content)
@@ -142,7 +147,9 @@ class ContentFile(File):
def __bool__(self):
return True
- __nonzero__ = __bool__ # Python 2
+
+ def __nonzero__(self): # Python 2 compatibility
+ return type(self).__bool__(self)
def open(self, mode=None):
self.seek(0)
diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py
index 23572465cf..0caf6b29fa 100644
--- a/django/core/handlers/base.py
+++ b/django/core/handlers/base.py
@@ -134,7 +134,7 @@ class BaseHandler(object):
raise ValueError("The view %s.%s didn't return an HttpResponse object." % (callback.__module__, view_name))
# If the response supports deferred rendering, apply template
- # response middleware and the render the response
+ # response middleware and then render the response
if hasattr(response, 'render') and callable(response.render):
for middleware_method in self._template_response_middleware:
response = middleware_method(request, response)
diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py
index 45cb2268ed..4c0710549a 100644
--- a/django/core/handlers/wsgi.py
+++ b/django/core/handlers/wsgi.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
+import codecs
import logging
import sys
from io import BytesIO
@@ -144,6 +145,14 @@ class WSGIRequest(http.HttpRequest):
self.META['PATH_INFO'] = path_info
self.META['SCRIPT_NAME'] = script_name
self.method = environ['REQUEST_METHOD'].upper()
+ _, content_params = self._parse_content_type(self.META.get('CONTENT_TYPE', ''))
+ if 'charset' in content_params:
+ try:
+ codecs.lookup(content_params['charset'])
+ except LookupError:
+ pass
+ else:
+ self.encoding = content_params['charset']
self._post_parse_error = False
try:
content_length = int(self.environ.get('CONTENT_LENGTH'))
@@ -155,6 +164,21 @@ class WSGIRequest(http.HttpRequest):
def _is_secure(self):
return 'wsgi.url_scheme' in self.environ and self.environ['wsgi.url_scheme'] == 'https'
+ def _parse_content_type(self, ctype):
+ """
+ Media Types parsing according to RFC 2616, section 3.7.
+
+ Returns the data type and parameters. For example:
+ Input: "text/plain; charset=iso-8859-1"
+ Output: ('text/plain', {'charset': 'iso-8859-1'})
+ """
+ content_type, _, params = ctype.partition(';')
+ content_params = {}
+ for parameter in params.split(';'):
+ k, _, v = parameter.strip().partition('=')
+ content_params[k] = v
+ return content_type, content_params
+
def _get_request(self):
if not hasattr(self, '_request'):
self._request = datastructures.MergeDict(self.POST, self.GET)
diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py
index 18437c6282..b6f7f560ed 100644
--- a/django/core/mail/backends/smtp.py
+++ b/django/core/mail/backends/smtp.py
@@ -7,6 +7,7 @@ from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.utils import DNS_NAME
from django.core.mail.message import sanitize_address
+from django.utils.encoding import force_bytes
class EmailBackend(BaseEmailBackend):
@@ -102,9 +103,11 @@ class EmailBackend(BaseEmailBackend):
from_email = sanitize_address(email_message.from_email, email_message.encoding)
recipients = [sanitize_address(addr, email_message.encoding)
for addr in email_message.recipients()]
+ message = email_message.message()
+ charset = message.get_charset().get_output_charset() if message.get_charset() else 'utf-8'
try:
self.connection.sendmail(from_email, recipients,
- email_message.message().as_string())
+ force_bytes(message.as_string(), charset))
except:
if not self.fail_silently:
raise
diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py
index c61ab2b663..fab5059376 100644
--- a/django/core/management/__init__.py
+++ b/django/core/management/__init__.py
@@ -9,6 +9,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import BaseCommand, CommandError, handle_default_options
from django.core.management.color import color_style
from django.utils.importlib import import_module
+from django.utils._os import upath
from django.utils import six
# For backwards compatibility: get_version() used to be in this module.
@@ -136,14 +137,15 @@ def call_command(name, *args, **options):
# Load the command object.
try:
app_name = get_commands()[name]
- if isinstance(app_name, BaseCommand):
- # If the command is already loaded, use it directly.
- klass = app_name
- else:
- klass = load_command_class(app_name, name)
except KeyError:
raise CommandError("Unknown command: %r" % name)
+ if isinstance(app_name, BaseCommand):
+ # If the command is already loaded, use it directly.
+ klass = app_name
+ else:
+ klass = load_command_class(app_name, name)
+
# Grab out a list of defaults from the options. optparse does this for us
# when the script runs from the command line, but since call_command can
# be called programatically, we need to simulate the loading and handling
@@ -409,10 +411,10 @@ def setup_environ(settings_mod, original_settings_path=None):
# Add this project to sys.path so that it's importable in the conventional
# way. For example, if this file (manage.py) lives in a directory
# "myproject", this code would add "/path/to/myproject" to sys.path.
- if '__init__.py' in settings_mod.__file__:
- p = os.path.dirname(settings_mod.__file__)
+ if '__init__.py' in upath(settings_mod.__file__):
+ p = os.path.dirname(upath(settings_mod.__file__))
else:
- p = settings_mod.__file__
+ p = upath(settings_mod.__file__)
project_directory, settings_filename = os.path.split(p)
if project_directory == os.curdir or not project_directory:
project_directory = os.getcwd()
diff --git a/django/core/management/commands/cleanup.py b/django/core/management/commands/cleanup.py
index e19d1649be..f83c64be8f 100644
--- a/django/core/management/commands/cleanup.py
+++ b/django/core/management/commands/cleanup.py
@@ -1,11 +1,11 @@
-from django.core.management.base import NoArgsCommand
-from django.utils import timezone
+import warnings
-class Command(NoArgsCommand):
- help = "Can be run as a cronjob or directly to clean out old data from the database (only expired sessions at the moment)."
+from django.contrib.sessions.management.commands import clearsessions
+
+class Command(clearsessions.Command):
def handle_noargs(self, **options):
- from django.db import transaction
- from django.contrib.sessions.models import Session
- Session.objects.filter(expire_date__lt=timezone.now()).delete()
- transaction.commit_unless_managed()
+ warnings.warn(
+ "The `cleanup` command has been deprecated in favor of `clearsessions`.",
+ PendingDeprecationWarning)
+ super(Command, self).handle_noargs(**options)
diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py
index b7392b9173..e1d8a33332 100644
--- a/django/core/management/commands/compilemessages.py
+++ b/django/core/management/commands/compilemessages.py
@@ -5,6 +5,7 @@ import os
import sys
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
+from django.utils._os import npath
def has_bom(fn):
with open(fn, 'rb') as f:
@@ -41,8 +42,8 @@ def compile_messages(stderr, locale=None):
# command, so that we can take advantage of shell quoting, to
# quote any malicious characters/escaping.
# See http://cyberelk.net/tim/articles/cmdline/ar01s02.html
- os.environ['djangocompilemo'] = pf + '.mo'
- os.environ['djangocompilepo'] = pf + '.po'
+ os.environ['djangocompilemo'] = npath(pf + '.mo')
+ os.environ['djangocompilepo'] = npath(pf + '.po')
if sys.platform == 'win32': # Different shell-variable syntax
cmd = 'msgfmt --check-format -o "%djangocompilemo%" "%djangocompilepo%"'
else:
diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py
index 32ae8abf5a..ed47b8fbf1 100644
--- a/django/core/management/commands/loaddata.py
+++ b/django/core/management/commands/loaddata.py
@@ -1,11 +1,9 @@
from __future__ import unicode_literals
-import sys
import os
import gzip
import zipfile
from optparse import make_option
-import traceback
from django.conf import settings
from django.core import serializers
@@ -15,6 +13,7 @@ from django.db import (connections, router, transaction, DEFAULT_DB_ALIAS,
IntegrityError, DatabaseError)
from django.db.models import get_apps
from django.utils.encoding import force_text
+from django.utils._os import upath
from itertools import product
try:
@@ -39,10 +38,10 @@ class Command(BaseCommand):
def handle(self, *fixture_labels, **options):
- ignore = options.get('ignore')
- using = options.get('database')
+ self.ignore = options.get('ignore')
+ self.using = options.get('database')
- connection = connections[using]
+ connection = connections[self.using]
if not len(fixture_labels):
raise CommandError(
@@ -50,8 +49,7 @@ class Command(BaseCommand):
"least one fixture in the command line."
)
- verbosity = int(options.get('verbosity'))
- show_traceback = options.get('traceback')
+ self.verbosity = int(options.get('verbosity'))
# commit is a stealth option - it isn't really useful as
# a command line option, but it can be useful when invoking
@@ -62,12 +60,10 @@ class Command(BaseCommand):
commit = options.get('commit', True)
# Keep a count of the installed objects and fixtures
- fixture_count = 0
- loaded_object_count = 0
- fixture_object_count = 0
- models = set()
-
- humanize = lambda dirname: "'%s'" % dirname if dirname else 'absolute path'
+ self.fixture_count = 0
+ self.loaded_object_count = 0
+ self.fixture_object_count = 0
+ self.models = set()
# Get a cursor (even though we don't need one yet). This has
# the side effect of initializing the test database (if
@@ -77,9 +73,9 @@ class Command(BaseCommand):
# Start transaction management. All fixtures are installed in a
# single transaction to ensure that all references are resolved.
if commit:
- transaction.commit_unless_managed(using=using)
- transaction.enter_transaction_management(using=using)
- transaction.managed(True, using=using)
+ transaction.commit_unless_managed(using=self.using)
+ transaction.enter_transaction_management(using=self.using)
+ transaction.managed(True, using=self.using)
class SingleZipReader(zipfile.ZipFile):
def __init__(self, *args, **kwargs):
@@ -89,136 +85,34 @@ class Command(BaseCommand):
def read(self):
return zipfile.ZipFile.read(self, self.namelist()[0])
- compression_types = {
+ self.compression_types = {
None: open,
'gz': gzip.GzipFile,
'zip': SingleZipReader
}
if has_bz2:
- compression_types['bz2'] = bz2.BZ2File
+ self.compression_types['bz2'] = bz2.BZ2File
app_module_paths = []
for app in get_apps():
if hasattr(app, '__path__'):
# It's a 'models/' subpackage
for path in app.__path__:
- app_module_paths.append(path)
+ app_module_paths.append(upath(path))
else:
# It's a models.py module
- app_module_paths.append(app.__file__)
+ app_module_paths.append(upath(app.__file__))
app_fixtures = [os.path.join(os.path.dirname(path), 'fixtures') for path in app_module_paths]
try:
with connection.constraint_checks_disabled():
for fixture_label in fixture_labels:
- parts = fixture_label.split('.')
-
- if len(parts) > 1 and parts[-1] in compression_types:
- compression_formats = [parts[-1]]
- parts = parts[:-1]
- else:
- compression_formats = compression_types.keys()
-
- if len(parts) == 1:
- fixture_name = parts[0]
- formats = serializers.get_public_serializer_formats()
- else:
- fixture_name, format = '.'.join(parts[:-1]), parts[-1]
- if format in serializers.get_public_serializer_formats():
- formats = [format]
- else:
- formats = []
-
- if formats:
- if verbosity >= 2:
- self.stdout.write("Loading '%s' fixtures..." % fixture_name)
- else:
- raise CommandError(
- "Problem installing fixture '%s': %s is not a known serialization format." %
- (fixture_name, format))
-
- if os.path.isabs(fixture_name):
- fixture_dirs = [fixture_name]
- else:
- fixture_dirs = app_fixtures + list(settings.FIXTURE_DIRS) + ['']
-
- for fixture_dir in fixture_dirs:
- if verbosity >= 2:
- self.stdout.write("Checking %s for fixtures..." % humanize(fixture_dir))
-
- label_found = False
- for combo in product([using, None], formats, compression_formats):
- database, format, compression_format = combo
- file_name = '.'.join(
- p for p in [
- fixture_name, database, format, compression_format
- ]
- if p
- )
-
- if verbosity >= 3:
- self.stdout.write("Trying %s for %s fixture '%s'..." % \
- (humanize(fixture_dir), file_name, fixture_name))
- full_path = os.path.join(fixture_dir, file_name)
- open_method = compression_types[compression_format]
- try:
- fixture = open_method(full_path, 'r')
- except IOError:
- if verbosity >= 2:
- self.stdout.write("No %s fixture '%s' in %s." % \
- (format, fixture_name, humanize(fixture_dir)))
- else:
- try:
- if label_found:
- raise CommandError("Multiple fixtures named '%s' in %s. Aborting." %
- (fixture_name, humanize(fixture_dir)))
-
- fixture_count += 1
- objects_in_fixture = 0
- loaded_objects_in_fixture = 0
- if verbosity >= 2:
- self.stdout.write("Installing %s fixture '%s' from %s." % \
- (format, fixture_name, humanize(fixture_dir)))
-
- objects = serializers.deserialize(format, fixture, using=using, ignorenonexistent=ignore)
-
- for obj in objects:
- objects_in_fixture += 1
- if router.allow_syncdb(using, obj.object.__class__):
- loaded_objects_in_fixture += 1
- models.add(obj.object.__class__)
- try:
- obj.save(using=using)
- except (DatabaseError, IntegrityError) as e:
- e.args = ("Could not load %(app_label)s.%(object_name)s(pk=%(pk)s): %(error_msg)s" % {
- 'app_label': obj.object._meta.app_label,
- 'object_name': obj.object._meta.object_name,
- 'pk': obj.object.pk,
- 'error_msg': force_text(e)
- },)
- raise
-
- loaded_object_count += loaded_objects_in_fixture
- fixture_object_count += objects_in_fixture
- label_found = True
- except Exception as e:
- if not isinstance(e, CommandError):
- e.args = ("Problem installing fixture '%s': %s" % (full_path, e),)
- raise
- finally:
- fixture.close()
-
- # If the fixture we loaded contains 0 objects, assume that an
- # error was encountered during fixture loading.
- if objects_in_fixture == 0:
- raise CommandError(
- "No fixture data found for '%s'. (File format may be invalid.)" %
- (fixture_name))
+ self.load_label(fixture_label, app_fixtures)
# Since we disabled constraint checks, we must manually check for
# any invalid keys that might have been added
- table_names = [model._meta.db_table for model in models]
+ table_names = [model._meta.db_table for model in self.models]
try:
connection.check_constraints(table_names=table_names)
except Exception as e:
@@ -229,31 +123,31 @@ class Command(BaseCommand):
raise
except Exception as e:
if commit:
- transaction.rollback(using=using)
- transaction.leave_transaction_management(using=using)
+ transaction.rollback(using=self.using)
+ transaction.leave_transaction_management(using=self.using)
raise
# If we found even one object in a fixture, we need to reset the
# database sequences.
- if loaded_object_count > 0:
- sequence_sql = connection.ops.sequence_reset_sql(no_style(), models)
+ if self.loaded_object_count > 0:
+ sequence_sql = connection.ops.sequence_reset_sql(no_style(), self.models)
if sequence_sql:
- if verbosity >= 2:
+ if self.verbosity >= 2:
self.stdout.write("Resetting sequences\n")
for line in sequence_sql:
cursor.execute(line)
if commit:
- transaction.commit(using=using)
- transaction.leave_transaction_management(using=using)
+ transaction.commit(using=self.using)
+ transaction.leave_transaction_management(using=self.using)
- if verbosity >= 1:
- if fixture_object_count == loaded_object_count:
+ if self.verbosity >= 1:
+ if self.fixture_object_count == self.loaded_object_count:
self.stdout.write("Installed %d object(s) from %d fixture(s)" % (
- loaded_object_count, fixture_count))
+ self.loaded_object_count, self.fixture_count))
else:
self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)" % (
- loaded_object_count, fixture_object_count, fixture_count))
+ self.loaded_object_count, self.fixture_object_count, self.fixture_count))
# Close the DB connection. This is required as a workaround for an
# edge case in MySQL: if the same connection is used to
@@ -261,3 +155,117 @@ class Command(BaseCommand):
# incorrect results. See Django #7572, MySQL #37735.
if commit:
connection.close()
+
+ def load_label(self, fixture_label, app_fixtures):
+
+ parts = fixture_label.split('.')
+
+ if len(parts) > 1 and parts[-1] in self.compression_types:
+ compression_formats = [parts[-1]]
+ parts = parts[:-1]
+ else:
+ compression_formats = self.compression_types.keys()
+
+ if len(parts) == 1:
+ fixture_name = parts[0]
+ formats = serializers.get_public_serializer_formats()
+ else:
+ fixture_name, format = '.'.join(parts[:-1]), parts[-1]
+ if format in serializers.get_public_serializer_formats():
+ formats = [format]
+ else:
+ formats = []
+
+ if formats:
+ if self.verbosity >= 2:
+ self.stdout.write("Loading '%s' fixtures..." % fixture_name)
+ else:
+ raise CommandError(
+ "Problem installing fixture '%s': %s is not a known serialization format." %
+ (fixture_name, format))
+
+ if os.path.isabs(fixture_name):
+ fixture_dirs = [fixture_name]
+ else:
+ fixture_dirs = app_fixtures + list(settings.FIXTURE_DIRS) + ['']
+
+ for fixture_dir in fixture_dirs:
+ self.process_dir(fixture_dir, fixture_name, compression_formats,
+ formats)
+
+ def process_dir(self, fixture_dir, fixture_name, compression_formats,
+ serialization_formats):
+
+ humanize = lambda dirname: "'%s'" % dirname if dirname else 'absolute path'
+
+ if self.verbosity >= 2:
+ self.stdout.write("Checking %s for fixtures..." % humanize(fixture_dir))
+
+ label_found = False
+ for combo in product([self.using, None], serialization_formats, compression_formats):
+ database, format, compression_format = combo
+ file_name = '.'.join(
+ p for p in [
+ fixture_name, database, format, compression_format
+ ]
+ if p
+ )
+
+ if self.verbosity >= 3:
+ self.stdout.write("Trying %s for %s fixture '%s'..." % \
+ (humanize(fixture_dir), file_name, fixture_name))
+ full_path = os.path.join(fixture_dir, file_name)
+ open_method = self.compression_types[compression_format]
+ try:
+ fixture = open_method(full_path, 'r')
+ except IOError:
+ if self.verbosity >= 2:
+ self.stdout.write("No %s fixture '%s' in %s." % \
+ (format, fixture_name, humanize(fixture_dir)))
+ else:
+ try:
+ if label_found:
+ raise CommandError("Multiple fixtures named '%s' in %s. Aborting." %
+ (fixture_name, humanize(fixture_dir)))
+
+ self.fixture_count += 1
+ objects_in_fixture = 0
+ loaded_objects_in_fixture = 0
+ if self.verbosity >= 2:
+ self.stdout.write("Installing %s fixture '%s' from %s." % \
+ (format, fixture_name, humanize(fixture_dir)))
+
+ objects = serializers.deserialize(format, fixture, using=self.using, ignorenonexistent=self.ignore)
+
+ for obj in objects:
+ objects_in_fixture += 1
+ if router.allow_syncdb(self.using, obj.object.__class__):
+ loaded_objects_in_fixture += 1
+ self.models.add(obj.object.__class__)
+ try:
+ obj.save(using=self.using)
+ except (DatabaseError, IntegrityError) as e:
+ e.args = ("Could not load %(app_label)s.%(object_name)s(pk=%(pk)s): %(error_msg)s" % {
+ 'app_label': obj.object._meta.app_label,
+ 'object_name': obj.object._meta.object_name,
+ 'pk': obj.object.pk,
+ 'error_msg': force_text(e)
+ },)
+ raise
+
+ self.loaded_object_count += loaded_objects_in_fixture
+ self.fixture_object_count += objects_in_fixture
+ label_found = True
+ except Exception as e:
+ if not isinstance(e, CommandError):
+ e.args = ("Problem installing fixture '%s': %s" % (full_path, e),)
+ raise
+ finally:
+ fixture.close()
+
+ # If the fixture we loaded contains 0 objects, assume that an
+ # error was encountered during fixture loading.
+ if objects_in_fixture == 0:
+ raise CommandError(
+ "No fixture data found for '%s'. (File format may be invalid.)" %
+ (fixture_name))
diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py
index 81c4fdf8cc..606cbe0b85 100644
--- a/django/core/management/commands/makemessages.py
+++ b/django/core/management/commands/makemessages.py
@@ -301,7 +301,7 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False,
locales = []
if locale is not None:
- locales.append(locale)
+ locales.append(str(locale))
elif all:
locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % localedir))
locales = [os.path.basename(l) for l in locale_dirs]
@@ -316,8 +316,8 @@ def make_messages(locale=None, domain='django', verbosity=1, all=False,
if not os.path.isdir(basedir):
os.makedirs(basedir)
- pofile = os.path.join(basedir, '%s.po' % domain)
- potfile = os.path.join(basedir, '%s.pot' % domain)
+ pofile = os.path.join(basedir, '%s.po' % str(domain))
+ potfile = os.path.join(basedir, '%s.pot' % str(domain))
if os.path.exists(potfile):
os.unlink(potfile)
diff --git a/django/core/management/sql.py b/django/core/management/sql.py
index 78cd17a23a..e46f4ae4f5 100644
--- a/django/core/management/sql.py
+++ b/django/core/management/sql.py
@@ -8,6 +8,7 @@ from django.conf import settings
from django.core.management.base import CommandError
from django.db import models
from django.db.models import get_models
+from django.utils._os import upath
def sql_create(app, style, connection):
@@ -145,21 +146,21 @@ def sql_all(app, style, connection):
def _split_statements(content):
comment_re = re.compile(r"^((?:'[^']*'|[^'])*?)--.*$")
statements = []
- statement = ""
+ statement = []
for line in content.split("\n"):
cleaned_line = comment_re.sub(r"\1", line).strip()
if not cleaned_line:
continue
- statement += cleaned_line
- if statement.endswith(";"):
- statements.append(statement)
- statement = ""
+ statement.append(cleaned_line)
+ if cleaned_line.endswith(";"):
+ statements.append(" ".join(statement))
+ statement = []
return statements
def custom_sql_for_model(model, style, connection):
opts = model._meta
- app_dir = os.path.normpath(os.path.join(os.path.dirname(models.get_app(model._meta.app_label).__file__), 'sql'))
+ app_dir = os.path.normpath(os.path.join(os.path.dirname(upath(models.get_app(model._meta.app_label).__file__)), 'sql'))
output = []
# Post-creation SQL should come before any initial SQL data is loaded.
diff --git a/django/core/management/templates.py b/django/core/management/templates.py
index d34a0deb7e..f522097b8c 100644
--- a/django/core/management/templates.py
+++ b/django/core/management/templates.py
@@ -8,8 +8,6 @@ import shutil
import stat
import sys
import tempfile
-import codecs
-
try:
from urllib.request import urlretrieve
except ImportError: # Python 2
@@ -156,12 +154,14 @@ class TemplateCommand(BaseCommand):
# Only render the Python files, as we don't want to
# accidentally render Django templates files
- with codecs.open(old_path, 'r', 'utf-8') as template_file:
+ with open(old_path, 'rb') as template_file:
content = template_file.read()
if filename.endswith(extensions) or filename in extra_files:
+ content = content.decode('utf-8')
template = Template(content)
content = template.render(context)
- with codecs.open(new_path, 'w', 'utf-8') as new_file:
+ content = content.encode('utf-8')
+ with open(new_path, 'wb') as new_file:
new_file.write(content)
if self.verbosity >= 2:
diff --git a/django/core/management/validation.py b/django/core/management/validation.py
index 957a712b72..c0452c5aa6 100644
--- a/django/core/management/validation.py
+++ b/django/core/management/validation.py
@@ -1,3 +1,4 @@
+import collections
import sys
from django.conf import settings
@@ -34,7 +35,7 @@ def get_validation_errors(outfile, app=None):
for (app_name, error) in get_app_errors().items():
e.add(app_name, error)
- for cls in models.get_models(app):
+ for cls in models.get_models(app, include_swapped=True):
opts = cls._meta
# Check swappable attribute.
@@ -137,16 +138,17 @@ def get_validation_errors(outfile, app=None):
# fields, m2m fields, m2m related objects or related objects
if f.rel:
if f.rel.to not in models.get_models():
- e.add(opts, "'%s' has a relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to))
+ # If the related model is swapped, provide a hint;
+ # otherwise, the model just hasn't been installed.
+ if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped:
+ e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
+ else:
+ e.add(opts, "'%s' has a relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to))
# it is a string and we could not find the model it refers to
# so skip the next section
if isinstance(f.rel.to, six.string_types):
continue
- # Make sure the model we're related hasn't been swapped out
- if f.rel.to._meta.swapped:
- e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
-
# Make sure the related field specified by a ForeignKey is unique
if not f.rel.to._meta.get_field(f.rel.field_name).unique:
e.add(opts, "Field '%s' under model '%s' must have a unique=True constraint." % (f.rel.field_name, f.rel.to.__name__))
@@ -183,16 +185,18 @@ def get_validation_errors(outfile, app=None):
# existing fields, m2m fields, m2m related objects or related
# objects
if f.rel.to not in models.get_models():
- e.add(opts, "'%s' has an m2m relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to))
+ # If the related model is swapped, provide a hint;
+ # otherwise, the model just hasn't been installed.
+ if not isinstance(f.rel.to, six.string_types) and f.rel.to._meta.swapped:
+ e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
+ else:
+ e.add(opts, "'%s' has an m2m relation with model %s, which has either not been installed or is abstract." % (f.name, f.rel.to))
+
# it is a string and we could not find the model it refers to
# so skip the next section
if isinstance(f.rel.to, six.string_types):
continue
- # Make sure the model we're related hasn't been swapped out
- if f.rel.to._meta.swapped:
- e.add(opts, "'%s' defines a relation with the model '%s.%s', which has been swapped out. Update the relation to point at settings.%s." % (f.name, f.rel.to._meta.app_label, f.rel.to._meta.object_name, f.rel.to._meta.swappable))
-
# Check that the field is not set to unique. ManyToManyFields do not support unique.
if f.unique:
e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name)
@@ -327,15 +331,29 @@ def get_validation_errors(outfile, app=None):
# Check unique_together.
for ut in opts.unique_together:
- for field_name in ut:
- try:
- f = opts.get_field(field_name, many_to_many=True)
- except models.FieldDoesNotExist:
- e.add(opts, '"unique_together" refers to %s, a field that doesn\'t exist. Check your syntax.' % field_name)
- else:
- if isinstance(f.rel, models.ManyToManyRel):
- e.add(opts, '"unique_together" refers to %s. ManyToManyFields are not supported in unique_together.' % f.name)
- if f not in opts.local_fields:
- e.add(opts, '"unique_together" refers to %s. This is not in the same model as the unique_together statement.' % f.name)
+ validate_local_fields(e, opts, "unique_together", ut)
+ if not isinstance(opts.index_together, collections.Sequence):
+ e.add(opts, '"index_together" must a sequence')
+ else:
+ for it in opts.index_together:
+ validate_local_fields(e, opts, "index_together", it)
return len(e.errors)
+
+
+def validate_local_fields(e, opts, field_name, fields):
+ from django.db import models
+
+ if not isinstance(fields, collections.Sequence):
+ e.add(opts, 'all %s elements must be sequences' % field_name)
+ else:
+ for field in fields:
+ try:
+ f = opts.get_field(field, many_to_many=True)
+ except models.FieldDoesNotExist:
+ e.add(opts, '"%s" refers to %s, a field that doesn\'t exist.' % (field_name, field))
+ else:
+ if isinstance(f.rel, models.ManyToManyRel):
+ e.add(opts, '"%s" refers to %s. ManyToManyFields are not supported in %s.' % (field_name, f.name, field_name))
+ if f not in opts.local_fields:
+ e.add(opts, '"%s" refers to %s. This is not in the same model as the %s statement.' % (field_name, f.name, field_name))
diff --git a/django/core/paginator.py b/django/core/paginator.py
index 6b0b3542f8..9ccff51a34 100644
--- a/django/core/paginator.py
+++ b/django/core/paginator.py
@@ -1,16 +1,25 @@
+import collections
from math import ceil
+from django.utils import six
+
+
class InvalidPage(Exception):
pass
+
class PageNotAnInteger(InvalidPage):
pass
+
class EmptyPage(InvalidPage):
pass
+
class Paginator(object):
- def __init__(self, object_list, per_page, orphans=0, allow_empty_first_page=True):
+
+ def __init__(self, object_list, per_page, orphans=0,
+ allow_empty_first_page=True):
self.object_list = object_list
self.per_page = int(per_page)
self.orphans = int(orphans)
@@ -18,7 +27,9 @@ class Paginator(object):
self._num_pages = self._count = None
def validate_number(self, number):
- "Validates the given 1-based page number."
+ """
+ Validates the given 1-based page number.
+ """
try:
number = int(number)
except (TypeError, ValueError):
@@ -33,16 +44,29 @@ class Paginator(object):
return number
def page(self, number):
- "Returns a Page object for the given 1-based page number."
+ """
+ Returns a Page object for the given 1-based page number.
+ """
number = self.validate_number(number)
bottom = (number - 1) * self.per_page
top = bottom + self.per_page
if top + self.orphans >= self.count:
top = self.count
- return Page(self.object_list[bottom:top], number, self)
+ return self._get_page(self.object_list[bottom:top], number, self)
+
+ def _get_page(self, *args, **kwargs):
+ """
+ Returns an instance of a single page.
+
+ This hook can be used by subclasses to use an alternative to the
+ standard :cls:`Page` object.
+ """
+ return Page(*args, **kwargs)
def _get_count(self):
- "Returns the total number of objects, across all pages."
+ """
+ Returns the total number of objects, across all pages.
+ """
if self._count is None:
try:
self._count = self.object_list.count()
@@ -55,7 +79,9 @@ class Paginator(object):
count = property(_get_count)
def _get_num_pages(self):
- "Returns the total number of pages."
+ """
+ Returns the total number of pages.
+ """
if self._num_pages is None:
if self.count == 0 and not self.allow_empty_first_page:
self._num_pages = 0
@@ -73,9 +99,12 @@ class Paginator(object):
return range(1, self.num_pages + 1)
page_range = property(_get_page_range)
-QuerySetPaginator = Paginator # For backwards-compatibility.
-class Page(object):
+QuerySetPaginator = Paginator # For backwards-compatibility.
+
+
+class Page(collections.Sequence):
+
def __init__(self, object_list, number, paginator):
self.object_list = object_list
self.number = number
@@ -88,40 +117,12 @@ class Page(object):
return len(self.object_list)
def __getitem__(self, index):
+ if not isinstance(index, (slice,) + six.integer_types):
+ raise TypeError
# The object_list is converted to a list so that if it was a QuerySet
# it won't be a database hit per __getitem__.
return list(self.object_list)[index]
- # The following four methods are only necessary for Python <2.6
- # compatibility (this class could just extend 2.6's collections.Sequence).
-
- def __iter__(self):
- i = 0
- try:
- while True:
- v = self[i]
- yield v
- i += 1
- except IndexError:
- return
-
- def __contains__(self, value):
- for v in self:
- if v == value:
- return True
- return False
-
- def index(self, value):
- for i, v in enumerate(self):
- if v == value:
- return i
- raise ValueError
-
- def count(self, value):
- return sum([1 for v in self if v == value])
-
- # End of compatibility methods.
-
def has_next(self):
return self.number < self.paginator.num_pages
diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py
index 276f9a4738..294934a04a 100644
--- a/django/core/serializers/base.py
+++ b/django/core/serializers/base.py
@@ -112,7 +112,7 @@ class Serializer(object):
if callable(getattr(self.stream, 'getvalue', None)):
return self.stream.getvalue()
-class Deserializer(object):
+class Deserializer(six.Iterator):
"""
Abstract base deserializer class.
"""
@@ -138,8 +138,6 @@ class Deserializer(object):
"""Iteration iterface -- return the next item in the stream"""
raise NotImplementedError
- next = __next__ # Python 2 compatibility
-
class DeserializedObject(object):
"""
A deserialized model.
diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py
index 666587dc77..ea333a22bd 100644
--- a/django/core/serializers/xml_serializer.py
+++ b/django/core/serializers/xml_serializer.py
@@ -161,8 +161,6 @@ class Deserializer(base.Deserializer):
return self._handle_object(node)
raise StopIteration
- next = __next__ # Python 2 compatibility
-
def _handle_object(self, node):
"""
Convert an <object> node to a DeserializedObject.
diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py
index a7004f2c2f..7387d13199 100644
--- a/django/core/servers/basehttp.py
+++ b/django/core/servers/basehttp.py
@@ -138,6 +138,10 @@ class WSGIRequestHandler(simple_server.WSGIRequestHandler, object):
self.style = color_style()
super(WSGIRequestHandler, self).__init__(*args, **kwargs)
+ def address_string(self):
+ # Short-circuit parent method to not call socket.getfqdn
+ return self.client_address[0]
+
def log_message(self, format, *args):
# Don't bother logging requests for admin images or the favicon.
if (self.path.startswith(self.admin_static_prefix)
diff --git a/django/core/signing.py b/django/core/signing.py
index 147e54780c..92ab968123 100644
--- a/django/core/signing.py
+++ b/django/core/signing.py
@@ -97,10 +97,10 @@ class JSONSerializer(object):
signing.loads.
"""
def dumps(self, obj):
- return json.dumps(obj, separators=(',', ':'))
+ return json.dumps(obj, separators=(',', ':')).encode('latin-1')
def loads(self, data):
- return json.loads(data)
+ return json.loads(data.decode('latin-1'))
def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
@@ -116,8 +116,10 @@ def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer,
only valid for a given namespace. Leaving this at the default
value or re-using a salt value across different parts of your
application without good cause is a security risk.
+
+ The serializer is expected to return a bytestring.
"""
- data = force_bytes(serializer().dumps(obj))
+ data = serializer().dumps(obj)
# Flag for if it's been compressed or not
is_compressed = False
@@ -136,20 +138,22 @@ def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer,
def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, max_age=None):
"""
- Reverse of dumps(), raises BadSignature if signature fails
+ Reverse of dumps(), raises BadSignature if signature fails.
+
+ The serializer is expected to accept a bytestring.
"""
# TimestampSigner.unsign always returns unicode but base64 and zlib
# compression operate on bytes.
base64d = force_bytes(TimestampSigner(key, salt=salt).unsign(s, max_age=max_age))
decompress = False
- if base64d[0] == b'.':
+ if base64d[:1] == b'.':
# It's compressed; uncompress it first
base64d = base64d[1:]
decompress = True
data = b64_decode(base64d)
if decompress:
data = zlib.decompress(data)
- return serializer().loads(force_str(data))
+ return serializer().loads(data)
class Signer(object):
diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
index af3df83d0a..c657fd9a54 100644
--- a/django/core/urlresolvers.py
+++ b/django/core/urlresolvers.py
@@ -16,6 +16,7 @@ from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.datastructures import MultiValueDict
from django.utils.encoding import force_str, force_text, iri_to_uri
from django.utils.functional import memoize, lazy
+from django.utils.http import urlquote
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
from django.utils.regex_helper import normalize
@@ -250,9 +251,9 @@ class RegexURLResolver(LocaleRegexProvider):
urlconf_repr = '<%s list>' % self.urlconf_name[0].__class__.__name__
else:
urlconf_repr = repr(self.urlconf_name)
- return force_str('<%s %s (%s:%s) %s>' % (
+ return str('<%s %s (%s:%s) %s>') % (
self.__class__.__name__, urlconf_repr, self.app_name,
- self.namespace, self.regex.pattern))
+ self.namespace, self.regex.pattern)
def _populate(self):
lookups = MultiValueDict()
@@ -379,14 +380,15 @@ class RegexURLResolver(LocaleRegexProvider):
except (ImportError, AttributeError) as e:
raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
possibilities = self.reverse_dict.getlist(lookup_view)
- prefix_norm, prefix_args = normalize(_prefix)[0]
+
+ prefix_norm, prefix_args = normalize(urlquote(_prefix))[0]
for possibility, pattern, defaults in possibilities:
for result, params in possibility:
if args:
if len(args) != len(params) + len(prefix_args):
continue
unicode_args = [force_text(val) for val in args]
- candidate = (prefix_norm + result) % dict(zip(prefix_args + params, unicode_args))
+ candidate = (prefix_norm + result) % dict(zip(prefix_args + params, unicode_args))
else:
if set(kwargs.keys()) | set(defaults.keys()) != set(params) | set(defaults.keys()) | set(prefix_args):
continue
@@ -398,8 +400,8 @@ class RegexURLResolver(LocaleRegexProvider):
if not matches:
continue
unicode_kwargs = dict([(k, force_text(v)) for (k, v) in kwargs.items()])
- candidate = (prefix_norm + result) % unicode_kwargs
- if re.search('^%s%s' % (_prefix, pattern), candidate, re.UNICODE):
+ candidate = (prefix_norm.replace('%', '%%') + result) % unicode_kwargs
+ if re.search('^%s%s' % (prefix_norm, pattern), candidate, re.UNICODE):
return candidate
# lookup_view can be URL label, or dotted path, or callable, Any of
# these can be passed in at the top, but callables are not friendly in
diff --git a/django/db/__init__.py b/django/db/__init__.py
index 26c7add0af..b1980488df 100644
--- a/django/db/__init__.py
+++ b/django/db/__init__.py
@@ -8,7 +8,7 @@ __all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
'IntegrityError', 'DEFAULT_DB_ALIAS')
-if DEFAULT_DB_ALIAS not in settings.DATABASES:
+if settings.DATABASES and DEFAULT_DB_ALIAS not in settings.DATABASES:
raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS)
connections = ConnectionHandler(settings.DATABASES)
diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py
index 28024c6428..0b5ba55cd0 100644
--- a/django/db/backends/__init__.py
+++ b/django/db/backends/__init__.py
@@ -181,8 +181,6 @@ class BaseDatabaseWrapper(object):
"""
if self.transaction_state:
return self.transaction_state[-1]
- # Note that this setting isn't documented, and is only used here, and
- # in enter_transaction_management()
return settings.TRANSACTIONS_MANAGED
def managed(self, flag=True):
@@ -904,16 +902,14 @@ class BaseDatabaseOperations(object):
Coerce the value returned by the database backend into a consistent type
that is compatible with the field type.
"""
- internal_type = field.get_internal_type()
- if internal_type == 'DecimalField':
+ if value is None:
return value
- elif internal_type == 'FloatField':
+ internal_type = field.get_internal_type()
+ if internal_type == 'FloatField':
return float(value)
elif (internal_type and (internal_type.endswith('IntegerField')
or internal_type == 'AutoField')):
return int(value)
- elif internal_type in ('DateField', 'DateTimeField', 'TimeField'):
- return value
return value
def check_aggregate_support(self, aggregate_func):
@@ -935,6 +931,11 @@ class BaseDatabaseOperations(object):
conn = ' %s ' % connector
return conn.join(sub_expressions)
+ def modify_insert_params(self, placeholders, params):
+ """Allow modification of insert parameters. Needed for Oracle Spatial
+ backend due to #10888.
+ """
+ return params
class BaseDatabaseIntrospection(object):
"""
diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py
index 69eee35352..9c3095d0aa 100644
--- a/django/db/backends/creation.py
+++ b/django/db/backends/creation.py
@@ -179,34 +179,47 @@ class BaseDatabaseCreation(object):
output = []
for f in model._meta.local_fields:
output.extend(self.sql_indexes_for_field(model, f, style))
+ for fs in model._meta.index_together:
+ fields = [model._meta.get_field_by_name(f)[0] for f in fs]
+ output.extend(self.sql_indexes_for_fields(model, fields, style))
return output
def sql_indexes_for_field(self, model, f, style):
"""
Return the CREATE INDEX SQL statements for a single model field.
"""
+ if f.db_index and not f.unique:
+ return self.sql_indexes_for_fields(model, [f], style)
+ else:
+ return []
+
+ def sql_indexes_for_fields(self, model, fields, style):
from django.db.backends.util import truncate_name
- if f.db_index and not f.unique:
- qn = self.connection.ops.quote_name
- tablespace = f.db_tablespace or model._meta.db_tablespace
- if tablespace:
- tablespace_sql = self.connection.ops.tablespace_sql(tablespace)
- if tablespace_sql:
- tablespace_sql = ' ' + tablespace_sql
- else:
- tablespace_sql = ''
- i_name = '%s_%s' % (model._meta.db_table, self._digest(f.column))
- output = [style.SQL_KEYWORD('CREATE INDEX') + ' ' +
- style.SQL_TABLE(qn(truncate_name(
- i_name, self.connection.ops.max_name_length()))) + ' ' +
- style.SQL_KEYWORD('ON') + ' ' +
- style.SQL_TABLE(qn(model._meta.db_table)) + ' ' +
- "(%s)" % style.SQL_FIELD(qn(f.column)) +
- "%s;" % tablespace_sql]
+ if len(fields) == 1 and fields[0].db_tablespace:
+ tablespace_sql = self.connection.ops.tablespace_sql(fields[0].db_tablespace)
+ elif model._meta.db_tablespace:
+ tablespace_sql = self.connection.ops.tablespace_sql(model._meta.db_tablespace)
else:
- output = []
- return output
+ tablespace_sql = ""
+ if tablespace_sql:
+ tablespace_sql = " " + tablespace_sql
+
+ field_names = []
+ qn = self.connection.ops.quote_name
+ for f in fields:
+ field_names.append(style.SQL_FIELD(qn(f.column)))
+
+ index_name = "%s_%s" % (model._meta.db_table, self._digest([f.name for f in fields]))
+
+ return [
+ style.SQL_KEYWORD("CREATE INDEX") + " " +
+ style.SQL_TABLE(qn(truncate_name(index_name, self.connection.ops.max_name_length()))) + " " +
+ style.SQL_KEYWORD("ON") + " " +
+ style.SQL_TABLE(qn(model._meta.db_table)) + " " +
+ "(%s)" % style.SQL_FIELD(", ".join(field_names)) +
+ "%s;" % tablespace_sql,
+ ]
def sql_destroy_model(self, model, references_to_delete, style):
"""
@@ -270,6 +283,7 @@ class BaseDatabaseCreation(object):
self._create_test_db(verbosity, autoclobber)
self.connection.close()
+ settings.DATABASES[self.connection.alias]["NAME"] = test_database_name
self.connection.settings_dict["NAME"] = test_database_name
# Report syncdb messages at one level lower than that requested.
diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
index 797406859c..f782ca80ae 100644
--- a/django/db/backends/mysql/base.py
+++ b/django/db/backends/mysql/base.py
@@ -374,47 +374,56 @@ class DatabaseWrapper(BaseDatabaseWrapper):
self.connection.ping()
return True
except DatabaseError:
- self.connection.close()
- self.connection = None
+ self.close()
return False
+ def get_connection_params(self):
+ kwargs = {
+ 'conv': django_conversions,
+ 'charset': 'utf8',
+ 'use_unicode': True,
+ }
+ settings_dict = self.settings_dict
+ if settings_dict['USER']:
+ kwargs['user'] = settings_dict['USER']
+ if settings_dict['NAME']:
+ kwargs['db'] = settings_dict['NAME']
+ if settings_dict['PASSWORD']:
+ kwargs['passwd'] = force_str(settings_dict['PASSWORD'])
+ if settings_dict['HOST'].startswith('/'):
+ kwargs['unix_socket'] = settings_dict['HOST']
+ elif settings_dict['HOST']:
+ kwargs['host'] = settings_dict['HOST']
+ if settings_dict['PORT']:
+ kwargs['port'] = int(settings_dict['PORT'])
+ # We need the number of potentially affected rows after an
+ # "UPDATE", not the number of changed rows.
+ kwargs['client_flag'] = CLIENT.FOUND_ROWS
+ kwargs.update(settings_dict['OPTIONS'])
+ return kwargs
+
+ def get_new_connection(self, conn_params):
+ conn = Database.connect(**conn_params)
+ conn.encoders[SafeText] = conn.encoders[six.text_type]
+ conn.encoders[SafeBytes] = conn.encoders[bytes]
+ return conn
+
+ def init_connection_state(self):
+ cursor = self.connection.cursor()
+ # SQL_AUTO_IS_NULL in MySQL controls whether an AUTO_INCREMENT column
+ # on a recently-inserted row will return when the field is tested for
+ # NULL. Disabling this value brings this aspect of MySQL in line with
+ # SQL standards.
+ cursor.execute('SET SQL_AUTO_IS_NULL = 0')
+ cursor.close()
+
def _cursor(self):
- new_connection = False
if not self._valid_connection():
- new_connection = True
- kwargs = {
- 'conv': django_conversions,
- 'charset': 'utf8',
- 'use_unicode': True,
- }
- settings_dict = self.settings_dict
- if settings_dict['USER']:
- kwargs['user'] = settings_dict['USER']
- if settings_dict['NAME']:
- kwargs['db'] = settings_dict['NAME']
- if settings_dict['PASSWORD']:
- kwargs['passwd'] = force_str(settings_dict['PASSWORD'])
- if settings_dict['HOST'].startswith('/'):
- kwargs['unix_socket'] = settings_dict['HOST']
- elif settings_dict['HOST']:
- kwargs['host'] = settings_dict['HOST']
- if settings_dict['PORT']:
- kwargs['port'] = int(settings_dict['PORT'])
- # We need the number of potentially affected rows after an
- # "UPDATE", not the number of changed rows.
- kwargs['client_flag'] = CLIENT.FOUND_ROWS
- kwargs.update(settings_dict['OPTIONS'])
- self.connection = Database.connect(**kwargs)
- self.connection.encoders[SafeText] = self.connection.encoders[six.text_type]
- self.connection.encoders[SafeBytes] = self.connection.encoders[bytes]
+ conn_params = self.get_connection_params()
+ self.connection = self.get_new_connection(conn_params)
+ self.init_connection_state()
connection_created.send(sender=self.__class__, connection=self)
cursor = self.connection.cursor()
- if new_connection:
- # SQL_AUTO_IS_NULL in MySQL controls whether an AUTO_INCREMENT column
- # on a recently-inserted row will return when the field is tested for
- # NULL. Disabling this value brings this aspect of MySQL in line with
- # SQL standards.
- cursor.execute('SET SQL_AUTO_IS_NULL = 0')
return CursorWrapper(cursor)
def _rollback(self):
@@ -435,8 +444,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
server_info = self.connection.get_server_info()
if new_connection:
# Make sure we close the connection
- self.connection.close()
- self.connection = None
+ self.close()
m = server_version_re.match(server_info)
if not m:
raise Exception('Unable to determine MySQL version from version string %r' % server_info)
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index f4b11ec327..129b5d827b 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -257,6 +257,10 @@ WHEN (new.%(col_name)s IS NULL)
if not name.startswith('"') and not name.endswith('"'):
name = '"%s"' % util.truncate_name(name.upper(),
self.max_name_length())
+ # Oracle puts the query text into a (query % args) construct, so % signs
+ # in names need to be escaped. The '%%' will be collapsed back to '%' at
+ # that stage so we aren't really making the name longer here.
+ name = name.replace('%','%%')
return name.upper()
def random_function_sql(self):
@@ -486,66 +490,78 @@ class DatabaseWrapper(BaseDatabaseWrapper):
return "%s/%s@%s" % (settings_dict['USER'],
settings_dict['PASSWORD'], dsn)
- def _cursor(self):
- cursor = None
- if not self._valid_connection():
- conn_string = convert_unicode(self._connect_string())
- conn_params = self.settings_dict['OPTIONS'].copy()
- if 'use_returning_into' in conn_params:
- del conn_params['use_returning_into']
- self.connection = Database.connect(conn_string, **conn_params)
- cursor = FormatStylePlaceholderCursor(self.connection)
- # Set the territory first. The territory overrides NLS_DATE_FORMAT
- # and NLS_TIMESTAMP_FORMAT to the territory default. When all of
- # these are set in single statement it isn't clear what is supposed
- # to happen.
- cursor.execute("ALTER SESSION SET NLS_TERRITORY = 'AMERICA'")
- # Set oracle date to ansi date format. This only needs to execute
- # once when we create a new connection. We also set the Territory
- # to 'AMERICA' which forces Sunday to evaluate to a '1' in
- # TO_CHAR().
- cursor.execute(
- "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
- " NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF'"
- + (" TIME_ZONE = 'UTC'" if settings.USE_TZ else ''))
+ def create_cursor(self, conn):
+ return FormatStylePlaceholderCursor(conn)
- if 'operators' not in self.__dict__:
- # Ticket #14149: Check whether our LIKE implementation will
- # work for this connection or we need to fall back on LIKEC.
- # This check is performed only once per DatabaseWrapper
- # instance per thread, since subsequent connections will use
- # the same settings.
- try:
- cursor.execute("SELECT 1 FROM DUAL WHERE DUMMY %s"
- % self._standard_operators['contains'],
- ['X'])
- except utils.DatabaseError:
- self.operators = self._likec_operators
- else:
- self.operators = self._standard_operators
+ def get_connection_params(self):
+ conn_params = self.settings_dict['OPTIONS'].copy()
+ if 'use_returning_into' in conn_params:
+ del conn_params['use_returning_into']
+ return conn_params
+ def get_new_connection(self, conn_params):
+ conn_string = convert_unicode(self._connect_string())
+ return Database.connect(conn_string, **conn_params)
+
+ def init_connection_state(self):
+ cursor = self.create_cursor(self.connection)
+ # Set the territory first. The territory overrides NLS_DATE_FORMAT
+ # and NLS_TIMESTAMP_FORMAT to the territory default. When all of
+ # these are set in single statement it isn't clear what is supposed
+ # to happen.
+ cursor.execute("ALTER SESSION SET NLS_TERRITORY = 'AMERICA'")
+ # Set oracle date to ansi date format. This only needs to execute
+ # once when we create a new connection. We also set the Territory
+ # to 'AMERICA' which forces Sunday to evaluate to a '1' in
+ # TO_CHAR().
+ cursor.execute(
+ "ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'"
+ " NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF'"
+ + (" TIME_ZONE = 'UTC'" if settings.USE_TZ else ''))
+ cursor.close()
+ if 'operators' not in self.__dict__:
+ # Ticket #14149: Check whether our LIKE implementation will
+ # work for this connection or we need to fall back on LIKEC.
+ # This check is performed only once per DatabaseWrapper
+ # instance per thread, since subsequent connections will use
+ # the same settings.
+ cursor = self.create_cursor(self.connection)
try:
- self.oracle_version = int(self.connection.version.split('.')[0])
- # There's no way for the DatabaseOperations class to know the
- # currently active Oracle version, so we do some setups here.
- # TODO: Multi-db support will need a better solution (a way to
- # communicate the current version).
- if self.oracle_version <= 9:
- self.ops.regex_lookup = self.ops.regex_lookup_9
- else:
- self.ops.regex_lookup = self.ops.regex_lookup_10
- except ValueError:
- pass
- try:
- self.connection.stmtcachesize = 20
- except:
- # Django docs specify cx_Oracle version 4.3.1 or higher, but
- # stmtcachesize is available only in 4.3.2 and up.
- pass
+ cursor.execute("SELECT 1 FROM DUAL WHERE DUMMY %s"
+ % self._standard_operators['contains'],
+ ['X'])
+ except utils.DatabaseError:
+ self.operators = self._likec_operators
+ else:
+ self.operators = self._standard_operators
+ cursor.close()
+
+ try:
+ self.oracle_version = int(self.connection.version.split('.')[0])
+ # There's no way for the DatabaseOperations class to know the
+ # currently active Oracle version, so we do some setups here.
+ # TODO: Multi-db support will need a better solution (a way to
+ # communicate the current version).
+ if self.oracle_version <= 9:
+ self.ops.regex_lookup = self.ops.regex_lookup_9
+ else:
+ self.ops.regex_lookup = self.ops.regex_lookup_10
+ except ValueError:
+ pass
+ try:
+ self.connection.stmtcachesize = 20
+ except:
+ # Django docs specify cx_Oracle version 4.3.1 or higher, but
+ # stmtcachesize is available only in 4.3.2 and up.
+ pass
+
+ def _cursor(self):
+ if not self._valid_connection():
+ conn_params = self.get_connection_params()
+ self.connection = self.get_new_connection(conn_params)
+ self.init_connection_state()
connection_created.send(sender=self.__class__, connection=self)
- if not cursor:
- cursor = FormatStylePlaceholderCursor(self.connection)
- return cursor
+ return self.create_cursor(self.connection)
# Oracle doesn't support savepoint commits. Ignore them.
def _savepoint_commit(self, sid):
@@ -775,7 +791,7 @@ class FormatStylePlaceholderCursor(object):
return CursorIterator(self.cursor)
-class CursorIterator(object):
+class CursorIterator(six.Iterator):
"""Cursor iterator wrapper that invokes our custom row factory."""
@@ -789,8 +805,6 @@ class CursorIterator(object):
def __next__(self):
return _rowfactory(next(self.iter), self.cursor)
- next = __next__ # Python 2 compatibility
-
def _rowfactory(row, cursor):
# Cast numeric values as the appropriate Python type based upon the
diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py
index c4dc80503c..50e8d655a0 100644
--- a/django/db/backends/oracle/creation.py
+++ b/django/db/backends/oracle/creation.py
@@ -111,10 +111,12 @@ class DatabaseCreation(BaseDatabaseCreation):
print("Tests cancelled.")
sys.exit(1)
- self.connection.settings_dict['SAVED_USER'] = self.connection.settings_dict['USER']
- self.connection.settings_dict['SAVED_PASSWORD'] = self.connection.settings_dict['PASSWORD']
- self.connection.settings_dict['TEST_USER'] = self.connection.settings_dict['USER'] = TEST_USER
- self.connection.settings_dict['PASSWORD'] = TEST_PASSWD
+ from django.db import settings
+ real_settings = settings.DATABASES[self.connection.alias]
+ real_settings['SAVED_USER'] = self.connection.settings_dict['SAVED_USER'] = self.connection.settings_dict['USER']
+ real_settings['SAVED_PASSWORD'] = self.connection.settings_dict['SAVED_PASSWORD'] = self.connection.settings_dict['PASSWORD']
+ real_settings['TEST_USER'] = real_settings['USER'] = self.connection.settings_dict['TEST_USER'] = self.connection.settings_dict['USER'] = TEST_USER
+ real_settings['PASSWORD'] = self.connection.settings_dict['PASSWORD'] = TEST_PASSWD
return self.connection.settings_dict['NAME']
diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py
index cd26098caa..b9addfc926 100644
--- a/django/db/backends/postgresql_psycopg2/base.py
+++ b/django/db/backends/postgresql_psycopg2/base.py
@@ -159,48 +159,59 @@ class DatabaseWrapper(BaseDatabaseWrapper):
return self._pg_version
pg_version = property(_get_pg_version)
- def _cursor(self):
+ def get_connection_params(self):
settings_dict = self.settings_dict
- if self.connection is None:
- if not settings_dict['NAME']:
- from django.core.exceptions import ImproperlyConfigured
- raise ImproperlyConfigured(
- "settings.DATABASES is improperly configured. "
- "Please supply the NAME value.")
- conn_params = {
- 'database': settings_dict['NAME'],
- }
- conn_params.update(settings_dict['OPTIONS'])
- if 'autocommit' in conn_params:
- del conn_params['autocommit']
- if settings_dict['USER']:
- conn_params['user'] = settings_dict['USER']
- if settings_dict['PASSWORD']:
- conn_params['password'] = force_str(settings_dict['PASSWORD'])
- if settings_dict['HOST']:
- conn_params['host'] = settings_dict['HOST']
- if settings_dict['PORT']:
- conn_params['port'] = settings_dict['PORT']
- self.connection = Database.connect(**conn_params)
- self.connection.set_client_encoding('UTF8')
- tz = 'UTC' if settings.USE_TZ else settings_dict.get('TIME_ZONE')
- if tz:
- try:
- get_parameter_status = self.connection.get_parameter_status
- except AttributeError:
- # psycopg2 < 2.0.12 doesn't have get_parameter_status
- conn_tz = None
- else:
- conn_tz = get_parameter_status('TimeZone')
+ if not settings_dict['NAME']:
+ from django.core.exceptions import ImproperlyConfigured
+ raise ImproperlyConfigured(
+ "settings.DATABASES is improperly configured. "
+ "Please supply the NAME value.")
+ conn_params = {
+ 'database': settings_dict['NAME'],
+ }
+ conn_params.update(settings_dict['OPTIONS'])
+ if 'autocommit' in conn_params:
+ del conn_params['autocommit']
+ if settings_dict['USER']:
+ conn_params['user'] = settings_dict['USER']
+ if settings_dict['PASSWORD']:
+ conn_params['password'] = force_str(settings_dict['PASSWORD'])
+ if settings_dict['HOST']:
+ conn_params['host'] = settings_dict['HOST']
+ if settings_dict['PORT']:
+ conn_params['port'] = settings_dict['PORT']
+ return conn_params
+
+ def get_new_connection(self, conn_params):
+ return Database.connect(**conn_params)
- if conn_tz != tz:
- # Set the time zone in autocommit mode (see #17062)
- self.connection.set_isolation_level(
- psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
- self.connection.cursor().execute(
- self.ops.set_time_zone_sql(), [tz])
- self.connection.set_isolation_level(self.isolation_level)
- self._get_pg_version()
+ def init_connection_state(self):
+ settings_dict = self.settings_dict
+ self.connection.set_client_encoding('UTF8')
+ tz = 'UTC' if settings.USE_TZ else settings_dict.get('TIME_ZONE')
+ if tz:
+ try:
+ get_parameter_status = self.connection.get_parameter_status
+ except AttributeError:
+ # psycopg2 < 2.0.12 doesn't have get_parameter_status
+ conn_tz = None
+ else:
+ conn_tz = get_parameter_status('TimeZone')
+
+ if conn_tz != tz:
+ # Set the time zone in autocommit mode (see #17062)
+ self.connection.set_isolation_level(
+ psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
+ self.connection.cursor().execute(
+ self.ops.set_time_zone_sql(), [tz])
+ self.connection.set_isolation_level(self.isolation_level)
+ self._get_pg_version()
+
+ def _cursor(self):
+ if self.connection is None:
+ conn_params = self.get_connection_params()
+ self.connection = self.get_new_connection(conn_params)
+ self.init_connection_state()
connection_created.send(sender=self.__class__, connection=self)
cursor = self.connection.cursor()
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py
index 341fc13e81..aedf953cfd 100644
--- a/django/db/backends/sqlite3/base.py
+++ b/django/db/backends/sqlite3/base.py
@@ -123,8 +123,12 @@ class DatabaseOperations(BaseDatabaseOperations):
"""
SQLite has a compile-time default (SQLITE_LIMIT_VARIABLE_NUMBER) of
999 variables per query.
+
+ If there is just single field to insert, then we can hit another
+ limit, SQLITE_MAX_COMPOUND_SELECT which defaults to 500.
"""
- return (999 // len(fields)) if len(fields) > 0 else len(objs)
+ limit = 999 if len(fields) > 1 else 500
+ return (limit // len(fields)) if len(fields) > 0 else len(objs)
def date_extract_sql(self, lookup_type, field_name):
# sqlite doesn't support extract, so we fake it with the user-defined
@@ -230,7 +234,7 @@ class DatabaseOperations(BaseDatabaseOperations):
res.append("SELECT %s" % ", ".join(
"%%s AS %s" % self.quote_name(f.column) for f in fields
))
- res.extend(["UNION SELECT %s" % ", ".join(["%s"] * len(fields))] * (num_values - 1))
+ res.extend(["UNION ALL SELECT %s" % ", ".join(["%s"] * len(fields))] * (num_values - 1))
return " ".join(res)
class DatabaseWrapper(BaseDatabaseWrapper):
@@ -265,7 +269,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
self.introspection = DatabaseIntrospection(self)
self.validation = BaseDatabaseValidation(self)
- def _sqlite_create_connection(self):
+ def get_connection_params(self):
settings_dict = self.settings_dict
if not settings_dict['NAME']:
from django.core.exceptions import ImproperlyConfigured
@@ -292,12 +296,24 @@ class DatabaseWrapper(BaseDatabaseWrapper):
RuntimeWarning
)
kwargs.update({'check_same_thread': False})
- self.connection = Database.connect(**kwargs)
+ return kwargs
+
+ def get_new_connection(self, conn_params):
+ conn = Database.connect(**conn_params)
# Register extract, date_trunc, and regexp functions.
- self.connection.create_function("django_extract", 2, _sqlite_extract)
- self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc)
- self.connection.create_function("regexp", 2, _sqlite_regexp)
- self.connection.create_function("django_format_dtdelta", 5, _sqlite_format_dtdelta)
+ conn.create_function("django_extract", 2, _sqlite_extract)
+ conn.create_function("django_date_trunc", 2, _sqlite_date_trunc)
+ conn.create_function("regexp", 2, _sqlite_regexp)
+ conn.create_function("django_format_dtdelta", 5, _sqlite_format_dtdelta)
+ return conn
+
+ def init_connection_state(self):
+ pass
+
+ def _sqlite_create_connection(self):
+ conn_params = self.get_connection_params()
+ self.connection = self.get_new_connection(conn_params)
+ self.init_connection_state()
connection_created.send(sender=self.__class__, connection=self)
def _cursor(self):
diff --git a/django/db/backends/util.py b/django/db/backends/util.py
index e029c42899..1ba23060e0 100644
--- a/django/db/backends/util.py
+++ b/django/db/backends/util.py
@@ -24,11 +24,9 @@ class CursorWrapper(object):
self.db.set_dirty()
def __getattr__(self, attr):
- self.set_dirty()
- if attr in self.__dict__:
- return self.__dict__[attr]
- else:
- return getattr(self.cursor, attr)
+ if attr in ('execute', 'executemany', 'callproc'):
+ self.set_dirty()
+ return getattr(self.cursor, attr)
def __iter__(self):
return iter(self.cursor)
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 35c607ac2d..db228b3b58 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -419,6 +419,11 @@ class Model(six.with_metaclass(ModelBase, object)):
def __str__(self):
if not six.PY3 and hasattr(self, '__unicode__'):
+ if type(self).__unicode__ == Model.__str__:
+ klass_name = type(self).__name__
+ raise RuntimeError("%s.__unicode__ is aliased to __str__. Did"
+ " you apply @python_2_unicode_compatible"
+ " without defining __str__?" % klass_name)
return force_text(self).encode('utf-8')
return '%s object' % self.__class__.__name__
diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py
index 30c44bacde..3566d777c6 100644
--- a/django/db/models/expressions.py
+++ b/django/db/models/expressions.py
@@ -62,7 +62,9 @@ class ExpressionNode(tree.Node):
def __truediv__(self, other):
return self._combine(other, self.DIV, False)
- __div__ = __truediv__ # Python 2 compatibility
+
+ def __div__(self, other): # Python 2 compatibility
+ return type(self).__truediv__(self, other)
def __mod__(self, other):
return self._combine(other, self.MOD, False)
@@ -94,7 +96,9 @@ class ExpressionNode(tree.Node):
def __rtruediv__(self, other):
return self._combine(other, self.DIV, True)
- __rdiv__ = __rtruediv__ # Python 2 compatibility
+
+ def __rdiv__(self, other): # Python 2 compatibility
+ return type(self).__rtruediv__(self, other)
def __rmod__(self, other):
return self._combine(other, self.MOD, True)
@@ -151,10 +155,10 @@ class DateModifierNode(ExpressionNode):
(A custom function is used in order to preserve six digits of fractional
second information on sqlite, and to format both date and datetime values.)
- Note that microsecond comparisons are not well supported with MySQL, since
+ Note that microsecond comparisons are not well supported with MySQL, since
MySQL does not store microsecond information.
- Only adding and subtracting timedeltas is supported, attempts to use other
+ Only adding and subtracting timedeltas is supported, attempts to use other
operations raise a TypeError.
"""
def __init__(self, children, connector, negated=False):
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 92d35dc720..3bd426d2b3 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -54,14 +54,16 @@ def add_lazy_relation(cls, field, relation, operation):
else:
# Look for an "app.Model" relation
- try:
- app_label, model_name = relation.split(".")
- except ValueError:
- # If we can't split, assume a model in current app
- app_label = cls._meta.app_label
- model_name = relation
- except AttributeError:
- # If it doesn't have a split it's actually a model class
+
+ if isinstance(relation, six.string_types):
+ try:
+ app_label, model_name = relation.split(".")
+ except ValueError:
+ # If we can't split, assume a model in current app
+ app_label = cls._meta.app_label
+ model_name = relation
+ else:
+ # it's actually a model class
app_label = relation._meta.app_label
model_name = relation._meta.object_name
@@ -573,9 +575,31 @@ def create_many_related_manager(superclass, rel):
self.reverse = reverse
self.through = through
self.prefetch_cache_name = prefetch_cache_name
- self._pk_val = self.instance.pk
- if self._pk_val is None:
- raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__)
+ self._fk_val = self._get_fk_val(instance, source_field_name)
+ if self._fk_val is None:
+ raise ValueError('"%r" needs to have a value for field "%s" before '
+ 'this many-to-many relationship can be used.' %
+ (instance, source_field_name))
+ # Even if this relation is not to pk, we require still pk value.
+ # The wish is that the instance has been already saved to DB,
+ # although having a pk value isn't a guarantee of that.
+ if instance.pk is None:
+ raise ValueError("%r instance needs to have a primary key value before "
+ "a many-to-many relationship can be used." %
+ instance.__class__.__name__)
+
+
+ def _get_fk_val(self, obj, field_name):
+ """
+ Returns the correct value for this relationship's foreign key. This
+ might be something else than pk value when to_field is used.
+ """
+ fk = self.through._meta.get_field(field_name)
+ if fk.rel.field_name and fk.rel.field_name != fk.rel.to._meta.pk.attname:
+ attname = fk.rel.get_related_field().get_attname()
+ return fk.get_prep_lookup('exact', getattr(obj, attname))
+ else:
+ return obj.pk
def get_query_set(self):
try:
@@ -677,7 +701,11 @@ def create_many_related_manager(superclass, rel):
if not router.allow_relation(obj, self.instance):
raise ValueError('Cannot add "%r": instance is on database "%s", value is on database "%s"' %
(obj, self.instance._state.db, obj._state.db))
- new_ids.add(obj.pk)
+ fk_val = self._get_fk_val(obj, target_field_name)
+ if fk_val is None:
+ raise ValueError('Cannot add "%r": the value for field "%s" is None' %
+ (obj, target_field_name))
+ new_ids.add(self._get_fk_val(obj, target_field_name))
elif isinstance(obj, Model):
raise TypeError("'%s' instance expected, got %r" % (self.model._meta.object_name, obj))
else:
@@ -685,7 +713,7 @@ def create_many_related_manager(superclass, rel):
db = router.db_for_write(self.through, instance=self.instance)
vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True)
vals = vals.filter(**{
- source_field_name: self._pk_val,
+ source_field_name: self._fk_val,
'%s__in' % target_field_name: new_ids,
})
new_ids = new_ids - set(vals)
@@ -699,11 +727,12 @@ def create_many_related_manager(superclass, rel):
# Add the ones that aren't there already
self.through._default_manager.using(db).bulk_create([
self.through(**{
- '%s_id' % source_field_name: self._pk_val,
+ '%s_id' % source_field_name: self._fk_val,
'%s_id' % target_field_name: obj_id,
})
for obj_id in new_ids
])
+
if self.reverse or source_field_name == self.source_field_name:
# Don't send the signal when we are inserting the
# duplicate data row for symmetrical reverse entries.
@@ -722,7 +751,7 @@ def create_many_related_manager(superclass, rel):
old_ids = set()
for obj in objs:
if isinstance(obj, self.model):
- old_ids.add(obj.pk)
+ old_ids.add(self._get_fk_val(obj, target_field_name))
else:
old_ids.add(obj)
# Work out what DB we're operating on
@@ -736,7 +765,7 @@ def create_many_related_manager(superclass, rel):
model=self.model, pk_set=old_ids, using=db)
# Remove the specified objects from the join table
self.through._default_manager.using(db).filter(**{
- source_field_name: self._pk_val,
+ source_field_name: self._fk_val,
'%s__in' % target_field_name: old_ids
}).delete()
if self.reverse or source_field_name == self.source_field_name:
@@ -756,7 +785,7 @@ def create_many_related_manager(superclass, rel):
instance=self.instance, reverse=self.reverse,
model=self.model, pk_set=None, using=db)
self.through._default_manager.using(db).filter(**{
- source_field_name: self._pk_val
+ source_field_name: self._fk_val
}).delete()
if self.reverse or source_field_name == self.source_field_name:
# Don't send the signal when we are clearing the
@@ -1026,11 +1055,6 @@ class ForeignKey(RelatedField, Field):
def contribute_to_class(self, cls, name):
super(ForeignKey, self).contribute_to_class(cls, name)
setattr(cls, self.name, ReverseSingleRelatedObjectDescriptor(self))
- if isinstance(self.rel.to, six.string_types):
- target = self.rel.to
- else:
- target = self.rel.to._meta.db_table
- cls._meta.duplicate_targets[self.column] = (target, "o2m")
def contribute_to_related_class(self, cls, related):
# Internal FK's - i.e., those with a related name ending with '+' -
@@ -1269,12 +1293,6 @@ class ManyToManyField(RelatedField, Field):
field.rel.through = model
add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
- if isinstance(self.rel.to, six.string_types):
- target = self.rel.to
- else:
- target = self.rel.to._meta.db_table
- cls._meta.duplicate_targets[self.column] = (target, "m2m")
-
def contribute_to_related_class(self, cls, related):
# Internal M2Ms (i.e., those with a related name ending with '+')
# and swapped models don't get a related descriptor.
diff --git a/django/db/models/loading.py b/django/db/models/loading.py
index 6ae37505a5..a36608e3e1 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -5,6 +5,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.utils.datastructures import SortedDict
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
+from django.utils._os import upath
from django.utils import six
import imp
@@ -158,7 +159,7 @@ class AppCache(object):
def get_models(self, app_mod=None,
include_auto_created=False, include_deferred=False,
- only_installed=True):
+ only_installed=True, include_swapped=False):
"""
Given a module containing models, returns a list of the models.
Otherwise returns a list of all installed models.
@@ -170,8 +171,16 @@ class AppCache(object):
By default, models created to satisfy deferred attribute
queries are *not* included in the list of models. However, if
you specify include_deferred, they will be.
+
+ By default, models that aren't part of installed apps will *not*
+ be included in the list of models. However, if you specify
+ only_installed=False, they will be.
+
+ By default, models that have been swapped out will *not* be
+ included in the list of models. However, if you specify
+ include_swapped, they will be.
"""
- cache_key = (app_mod, include_auto_created, include_deferred, only_installed)
+ cache_key = (app_mod, include_auto_created, include_deferred, only_installed, include_swapped)
try:
return self._get_models_cache[cache_key]
except KeyError:
@@ -194,7 +203,8 @@ class AppCache(object):
model_list.extend(
model for model in app.values()
if ((not model._deferred or include_deferred) and
- (not model._meta.auto_created or include_auto_created))
+ (not model._meta.auto_created or include_auto_created) and
+ (not model._meta.swapped or include_swapped))
)
self._get_models_cache[cache_key] = model_list
return model_list
@@ -226,8 +236,8 @@ class AppCache(object):
# The same model may be imported via different paths (e.g.
# appname.models and project.appname.models). We use the source
# filename as a means to detect identity.
- fname1 = os.path.abspath(sys.modules[model.__module__].__file__)
- fname2 = os.path.abspath(sys.modules[model_dict[model_name].__module__].__file__)
+ fname1 = os.path.abspath(upath(sys.modules[model.__module__].__file__))
+ fname2 = os.path.abspath(upath(sys.modules[model_dict[model_name].__module__].__file__))
# Since the filename extension could be .py the first time and
# .pyc or .pyo the second time, ignore the extension when
# comparing.
diff --git a/django/db/models/options.py b/django/db/models/options.py
index ace2816b0b..e3b4b7ba1b 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -9,11 +9,10 @@ from django.db.models.fields.related import ManyToManyRel
from django.db.models.fields import AutoField, FieldDoesNotExist
from django.db.models.fields.proxy import OrderWrt
from django.db.models.loading import get_models, app_cache_ready
-from django.utils.translation import activate, deactivate_all, get_language, string_concat
-from django.utils.encoding import force_text, smart_text
-from django.utils.datastructures import SortedDict
from django.utils import six
-from django.utils.encoding import python_2_unicode_compatible
+from django.utils.datastructures import SortedDict
+from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible
+from django.utils.translation import activate, deactivate_all, get_language, string_concat
# Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces".
get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip()
@@ -21,7 +20,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|
DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
'unique_together', 'permissions', 'get_latest_by',
'order_with_respect_to', 'app_label', 'db_tablespace',
- 'abstract', 'managed', 'proxy', 'swappable', 'auto_created', 'auto_register')
+ 'abstract', 'managed', 'proxy', 'swappable', 'auto_created', 'index_together', 'auto_register')
@python_2_unicode_compatible
@@ -34,6 +33,7 @@ class Options(object):
self.db_table = ''
self.ordering = []
self.unique_together = []
+ self.index_together = []
self.permissions = []
self.object_name, self.app_label = None, app_label
self.get_latest_by = None
@@ -58,7 +58,6 @@ class Options(object):
self.concrete_model = None
self.swappable = None
self.parents = SortedDict()
- self.duplicate_targets = {}
self.auto_created = False
# To handle various inheritance situations, we need to track where
@@ -78,6 +77,7 @@ class Options(object):
from django.db.backends.util import truncate_name
cls._meta = self
+ self.model = cls
self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS
# First, construct the default values for these options.
self.object_name = cls.__name__
@@ -150,24 +150,6 @@ class Options(object):
auto_created=True)
model.add_to_class('id', auto)
- # Determine any sets of fields that are pointing to the same targets
- # (e.g. two ForeignKeys to the same remote model). The query
- # construction code needs to know this. At the end of this,
- # self.duplicate_targets will map each duplicate field column to the
- # columns it duplicates.
- collections = {}
- for column, target in six.iteritems(self.duplicate_targets):
- try:
- collections[target].add(column)
- except KeyError:
- collections[target] = set([column])
- self.duplicate_targets = {}
- for elt in six.itervalues(collections):
- if len(elt) == 1:
- continue
- for column in elt:
- self.duplicate_targets[column] = elt.difference(set([column]))
-
def add_field(self, field):
# Insert the given field in the order in which it was created, using
# the "creation_counter" attribute of the field.
@@ -195,6 +177,12 @@ class Options(object):
self.pk = field
field.serialize = False
+ def pk_index(self):
+ """
+ Returns the index of the primary key field in the self.fields list.
+ """
+ return self.fields.index(self.pk)
+
def setup_proxy(self, target):
"""
Does the internal setup so that the current model is a proxy for
@@ -481,7 +469,7 @@ class Options(object):
a granparent or even more distant relation.
"""
if not self.parents:
- return
+ return None
if model in self.parents:
return [model]
for parent in self.parents:
@@ -489,8 +477,7 @@ class Options(object):
if res:
res.insert(0, parent)
return res
- raise TypeError('%r is not an ancestor of this model'
- % model._meta.module_name)
+ return None
def get_parent_list(self):
"""
@@ -522,22 +509,3 @@ class Options(object):
# of the chain to the ancestor is that parent
# links
return self.parents[parent] or parent_link
-
- def get_ordered_objects(self):
- "Returns a list of Options objects that are ordered with respect to this object."
- if not hasattr(self, '_ordered_objects'):
- objects = []
- # TODO
- #for klass in get_models(get_app(self.app_label)):
- # opts = klass._meta
- # if opts.order_with_respect_to and opts.order_with_respect_to.rel \
- # and self == opts.order_with_respect_to.rel.to._meta:
- # objects.append(opts)
- self._ordered_objects = objects
- return self._ordered_objects
-
- def pk_index(self):
- """
- Returns the index of the primary key field in the self.fields list.
- """
- return self.fields.index(self.pk)
diff --git a/django/db/models/query.py b/django/db/models/query.py
index dc1ddf1606..f56d5d2842 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -5,6 +5,7 @@ The main QuerySet implementation. This provides the public API for the ORM.
import copy
import itertools
import sys
+import warnings
from django.core import exceptions
from django.db import connections, router, transaction, IntegrityError
@@ -135,7 +136,9 @@ class QuerySet(object):
except StopIteration:
return False
return True
- __nonzero__ = __bool__ # Python 2
+
+ 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
@@ -698,6 +701,9 @@ class QuerySet(object):
If fields are specified, they must be ForeignKey fields and only those
related objects are included in the selection.
"""
+ if 'depth' in kwargs:
+ warnings.warn('The "depth" keyword argument has been deprecated.\n'
+ 'Use related field names instead.', PendingDeprecationWarning)
depth = kwargs.pop('depth', 0)
if kwargs:
raise TypeError('Unexpected keyword arguments to select_related: %s'
@@ -1071,7 +1077,7 @@ class ValuesQuerySet(QuerySet):
def _as_sql(self, connection):
"""
- For ValueQuerySet (and subclasses like ValuesListQuerySet), they can
+ For ValuesQuerySet (and subclasses like ValuesListQuerySet), they can
only be used as nested queries if they're already set up to select only
a single field (in which case, that is the field column that is
returned). This differs from QuerySet.as_sql(), where the column to
@@ -1294,7 +1300,7 @@ class EmptyQuerySet(QuerySet):
value_annotation = False
def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
- only_load=None, local_only=False):
+ only_load=None, from_parent=None):
"""
Helper function that recursively returns an information for a klass, to be
used in get_cached_row. It exists just to compute this information only
@@ -1314,8 +1320,10 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
* only_load - if the query has had only() or defer() applied,
this is the list of field names that will be returned. If None,
the full field list for `klass` can be assumed.
- * local_only - Only populate local fields. This is used when
- following reverse select-related relations
+ * from_parent - the parent model used to get to this model
+
+ Note that when travelling from parent to child, we will only load child
+ fields which aren't in the parent.
"""
if max_depth and requested is None and cur_depth > max_depth:
# We've recursed deeply enough; stop now.
@@ -1341,7 +1349,9 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
for field, model in klass._meta.get_fields_with_model():
if field.name not in load_fields:
skip.add(field.attname)
- elif local_only and model is not None:
+ elif from_parent and issubclass(from_parent, model.__class__):
+ # Avoid loading fields already loaded for parent model for
+ # child models.
continue
else:
init_list.append(field.attname)
@@ -1355,16 +1365,22 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
else:
# Load all fields on klass
- # We trying to not populate field_names variable for perfomance reason.
- # If field_names variable is set, it is used to instantiate desired fields,
- # by passing **dict(zip(field_names, fields)) as kwargs to Model.__init__ method.
- # But kwargs version of Model.__init__ is slower, so we should avoid using
- # it when it is not really neccesary.
- if local_only and len(klass._meta.local_fields) != len(klass._meta.fields):
- field_count = len(klass._meta.local_fields)
- field_names = [f.attname for f in klass._meta.local_fields]
- else:
- field_count = len(klass._meta.fields)
+ field_count = len(klass._meta.fields)
+ # Check if we need to skip some parent fields.
+ if from_parent and len(klass._meta.local_fields) != len(klass._meta.fields):
+ # Only load those fields which haven't been already loaded into
+ # 'from_parent'.
+ non_seen_models = [p for p in klass._meta.get_parent_list()
+ if not issubclass(from_parent, p)]
+ # Load local fields, too...
+ non_seen_models.append(klass)
+ field_names = [f.attname for f in klass._meta.fields
+ if f.model in non_seen_models]
+ field_count = len(field_names)
+ # Try to avoid populating field_names variable for perfomance reasons.
+ # If field_names variable is set, we use **kwargs based model init
+ # which is slower than normal init.
+ if field_count == len(klass._meta.fields):
field_names = ()
restricted = requested is not None
@@ -1386,14 +1402,20 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
if o.field.unique and select_related_descend(o.field, restricted, requested,
only_load.get(o.model), reverse=True):
next = requested[o.field.related_query_name()]
+ parent = klass if issubclass(o.model, klass) else None
klass_info = get_klass_info(o.model, max_depth=max_depth, cur_depth=cur_depth+1,
- requested=next, only_load=only_load, local_only=True)
+ requested=next, only_load=only_load, from_parent=parent)
reverse_related_fields.append((o.field, klass_info))
+ if field_names:
+ pk_idx = field_names.index(klass._meta.pk.attname)
+ else:
+ pk_idx = klass._meta.pk_index()
- return klass, field_names, field_count, related_fields, reverse_related_fields
+ return klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx
-def get_cached_row(row, index_start, using, klass_info, offset=0):
+def get_cached_row(row, index_start, using, klass_info, offset=0,
+ parent_data=()):
"""
Helper function that recursively returns an object with the specified
related attributes already populated.
@@ -1408,25 +1430,29 @@ def get_cached_row(row, index_start, using, klass_info, offset=0):
* offset - the number of additional fields that are known to
exist in row for `klass`. This usually means the number of
annotated results on `klass`.
- * using - the database alias on which the query is being executed.
+ * using - the database alias on which the query is being executed.
* klass_info - result of the get_klass_info function
+ * parent_data - parent model data in format (field, value). Used
+ to populate the non-local fields of child models.
"""
if klass_info is None:
return None
- klass, field_names, field_count, related_fields, reverse_related_fields = klass_info
+ klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx = klass_info
+
fields = row[index_start : index_start + field_count]
- # If all the select_related columns are None, then the related
+ # If the pk column is None (or the Oracle equivalent ''), then the related
# object must be non-existent - set the relation to None.
- # Otherwise, construct the related object.
- if fields == (None,) * field_count:
+ if fields[pk_idx] == None or fields[pk_idx] == '':
obj = None
+ elif field_names:
+ fields = list(fields)
+ for rel_field, value in parent_data:
+ field_names.append(rel_field.attname)
+ fields.append(value)
+ obj = klass(**dict(zip(field_names, fields)))
else:
- if field_names:
- obj = klass(**dict(zip(field_names, fields)))
- else:
- obj = klass(*fields)
-
+ obj = klass(*fields)
# If an object was retrieved, set the database state.
if obj:
obj._state.db = using
@@ -1456,34 +1482,35 @@ def get_cached_row(row, index_start, using, klass_info, offset=0):
# Only handle the restricted case - i.e., don't do a depth
# descent into reverse relations unless explicitly requested
for f, klass_info in reverse_related_fields:
+ # Transfer data from this object to childs.
+ parent_data = []
+ for rel_field, rel_model in klass_info[0]._meta.get_fields_with_model():
+ if rel_model is not None and isinstance(obj, rel_model):
+ parent_data.append((rel_field, getattr(obj, rel_field.attname)))
# Recursively retrieve the data for the related object
- cached_row = get_cached_row(row, index_end, using, klass_info)
+ cached_row = get_cached_row(row, index_end, using, klass_info,
+ parent_data=parent_data)
# If the recursive descent found an object, populate the
# descriptor caches relevant to the object
if cached_row:
rel_obj, index_end = cached_row
if obj is not None:
- # If the field is unique, populate the
- # reverse descriptor cache
+ # populate the reverse descriptor cache
setattr(obj, f.related.get_cache_name(), rel_obj)
if rel_obj is not None:
# If the related object exists, populate
# the descriptor cache.
setattr(rel_obj, f.get_cache_name(), obj)
- # Now populate all the non-local field values
- # on the related object
- for rel_field, rel_model in rel_obj._meta.get_fields_with_model():
- if rel_model is not None:
+ # Populate related object caches using parent data.
+ for rel_field, _ in parent_data:
+ if rel_field.rel:
setattr(rel_obj, rel_field.attname, getattr(obj, rel_field.attname))
- # populate the field cache for any related object
- # that has already been retrieved
- if rel_field.rel:
- try:
- cached_obj = getattr(obj, rel_field.get_cache_name())
- setattr(rel_obj, rel_field.get_cache_name(), cached_obj)
- except AttributeError:
- # Related object hasn't been cached yet
- pass
+ try:
+ cached_obj = getattr(obj, rel_field.get_cache_name())
+ setattr(rel_obj, rel_field.get_cache_name(), cached_obj)
+ except AttributeError:
+ # Related object hasn't been cached yet
+ pass
return obj, index_end
diff --git a/django/db/models/signals.py b/django/db/models/signals.py
index 4666169bec..2ef54a7ca7 100644
--- a/django/db/models/signals.py
+++ b/django/db/models/signals.py
@@ -2,15 +2,16 @@ from django.dispatch import Signal
class_prepared = Signal(providing_args=["class"])
-pre_init = Signal(providing_args=["instance", "args", "kwargs"])
-post_init = Signal(providing_args=["instance"])
+pre_init = Signal(providing_args=["instance", "args", "kwargs"], use_caching=True)
+post_init = Signal(providing_args=["instance"], use_caching=True)
-pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"])
-post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"])
+pre_save = Signal(providing_args=["instance", "raw", "using", "update_fields"],
+ use_caching=True)
+post_save = Signal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True)
-pre_delete = Signal(providing_args=["instance", "using"])
-post_delete = Signal(providing_args=["instance", "using"])
+pre_delete = Signal(providing_args=["instance", "using"], use_caching=True)
+post_delete = Signal(providing_args=["instance", "using"], use_caching=True)
-post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"])
+post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive"], use_caching=True)
-m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"])
+m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True)
diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py
index b9095e503a..4d846fb438 100644
--- a/django/db/models/sql/compiler.py
+++ b/django/db/models/sql/compiler.py
@@ -6,7 +6,7 @@ from django.db.backends.util import truncate_name
from django.db.models.constants import LOOKUP_SEP
from django.db.models.query_utils import select_related_descend
from django.db.models.sql.constants import (SINGLE, MULTI, ORDER_DIR,
- GET_ITERATOR_CHUNK_SIZE)
+ GET_ITERATOR_CHUNK_SIZE, REUSE_ALL, SelectInfo)
from django.db.models.sql.datastructures import EmptyResultSet
from django.db.models.sql.expressions import SQLEvaluator
from django.db.models.sql.query import get_order_dir, Query
@@ -103,21 +103,12 @@ class SQLCompiler(object):
result.append('WHERE %s' % where)
params.extend(w_params)
- grouping, gb_params = self.get_grouping()
+ grouping, gb_params = self.get_grouping(ordering_group_by)
if grouping:
if distinct_fields:
raise NotImplementedError(
"annotate() + distinct(fields) not implemented.")
- if ordering:
- # If the backend can't group by PK (i.e., any database
- # other than MySQL), then any fields mentioned in the
- # ordering clause needs to be in the group by clause.
- if not self.connection.features.allows_group_by_pk:
- for col, col_params in ordering_group_by:
- if col not in grouping:
- grouping.append(str(col))
- gb_params.extend(col_params)
- else:
+ if not ordering:
ordering = self.connection.ops.force_no_ordering()
result.append('GROUP BY %s' % ', '.join(grouping))
params.extend(gb_params)
@@ -188,7 +179,7 @@ class SQLCompiler(object):
col_aliases = set()
if self.query.select:
only_load = self.deferred_to_columns()
- for col in self.query.select:
+ for col, _ in self.query.select:
if isinstance(col, (list, tuple)):
alias, column = col
table = self.query.alias_map[alias].table_name
@@ -233,7 +224,7 @@ class SQLCompiler(object):
for alias, aggregate in self.query.aggregate_select.items()
])
- for table, col in self.query.related_select_cols:
+ for (table, col), _ in self.query.related_select_cols:
r = '%s.%s' % (qn(table), qn(col))
if with_aliases and col in col_aliases:
c_alias = 'Col%d' % len(col_aliases)
@@ -249,7 +240,7 @@ class SQLCompiler(object):
return result
def get_default_columns(self, with_aliases=False, col_aliases=None,
- start_alias=None, opts=None, as_pairs=False, local_only=False):
+ start_alias=None, opts=None, as_pairs=False, from_parent=None):
"""
Computes the default columns for selecting every field in the base
model. Will sometimes be called to pull in related models (e.g. via
@@ -274,7 +265,8 @@ class SQLCompiler(object):
if start_alias:
seen = {None: start_alias}
for field, model in opts.get_fields_with_model():
- if local_only and model is not None:
+ if from_parent and model is not None and issubclass(from_parent, model):
+ # Avoid loading data for already loaded parents.
continue
if start_alias:
try:
@@ -282,7 +274,8 @@ class SQLCompiler(object):
except KeyError:
link_field = opts.get_ancestor_link(model)
alias = self.query.join((start_alias, model._meta.db_table,
- link_field.column, model._meta.pk.column))
+ link_field.column, model._meta.pk.column),
+ join_field=link_field)
seen[model] = alias
else:
# If we're starting from the base model of the queryset, the
@@ -378,7 +371,7 @@ class SQLCompiler(object):
else:
order = asc
result.append('%s %s' % (field, order))
- group_by.append((field, []))
+ group_by.append((str(field), []))
continue
col, order = get_order_dir(field, asc)
if col in self.query.aggregate_select:
@@ -456,8 +449,8 @@ class SQLCompiler(object):
"""
if not alias:
alias = self.query.get_initial_alias()
- field, target, opts, joins, _, _ = self.query.setup_joins(pieces,
- opts, alias, False)
+ field, target, opts, joins, _ = self.query.setup_joins(
+ pieces, opts, alias, REUSE_ALL)
# We will later on need to promote those joins that were added to the
# query afresh above.
joins_to_promote = [j for j in joins if self.query.alias_refcount[j] < 2]
@@ -509,20 +502,27 @@ class SQLCompiler(object):
qn = self.quote_name_unless_alias
qn2 = self.connection.ops.quote_name
first = True
+ from_params = []
for alias in self.query.tables:
if not self.query.alias_refcount[alias]:
continue
try:
- name, alias, join_type, lhs, lhs_col, col, nullable = self.query.alias_map[alias]
+ name, alias, join_type, lhs, lhs_col, col, _, join_field = self.query.alias_map[alias]
except KeyError:
# Extra tables can end up in self.tables, but not in the
# alias_map if they aren't in a join. That's OK. We skip them.
continue
alias_str = (alias != name and ' %s' % alias or '')
if join_type and not first:
- result.append('%s %s%s ON (%s.%s = %s.%s)'
- % (join_type, qn(name), alias_str, qn(lhs),
- qn2(lhs_col), qn(alias), qn2(col)))
+ if join_field and hasattr(join_field, 'get_extra_join_sql'):
+ extra_cond, extra_params = join_field.get_extra_join_sql(
+ self.connection, qn, lhs, alias)
+ from_params.extend(extra_params)
+ else:
+ extra_cond = ""
+ result.append('%s %s%s ON (%s.%s = %s.%s%s)' %
+ (join_type, qn(name), alias_str, qn(lhs),
+ qn2(lhs_col), qn(alias), qn2(col), extra_cond))
else:
connector = not first and ', ' or ''
result.append('%s%s%s' % (connector, qn(name), alias_str))
@@ -536,45 +536,58 @@ class SQLCompiler(object):
connector = not first and ', ' or ''
result.append('%s%s' % (connector, qn(alias)))
first = False
- return result, []
+ return result, from_params
- def get_grouping(self):
+ def get_grouping(self, ordering_group_by):
"""
Returns a tuple representing the SQL elements in the "group by" clause.
"""
qn = self.quote_name_unless_alias
result, params = [], []
if self.query.group_by is not None:
- if (len(self.query.model._meta.fields) == len(self.query.select) and
- self.connection.features.allows_group_by_pk):
+ select_cols = self.query.select + self.query.related_select_cols
+ # Just the column, not the fields.
+ select_cols = [s[0] for s in select_cols]
+ if (len(self.query.model._meta.fields) == len(self.query.select)
+ and self.connection.features.allows_group_by_pk):
self.query.group_by = [
(self.query.model._meta.db_table, self.query.model._meta.pk.column)
]
-
- group_by = self.query.group_by or []
-
- extra_selects = []
- for extra_select, extra_params in six.itervalues(self.query.extra_select):
- extra_selects.append(extra_select)
- params.extend(extra_params)
- cols = (group_by + self.query.select +
- self.query.related_select_cols + extra_selects)
+ select_cols = []
seen = set()
+ cols = self.query.group_by + select_cols
for col in cols:
- if col in seen:
- continue
- seen.add(col)
if isinstance(col, (list, tuple)):
- result.append('%s.%s' % (qn(col[0]), qn(col[1])))
+ sql = '%s.%s' % (qn(col[0]), qn(col[1]))
elif hasattr(col, 'as_sql'):
- result.append(col.as_sql(qn, self.connection))
+ sql = col.as_sql(qn, self.connection)
else:
- result.append('(%s)' % str(col))
+ sql = '(%s)' % str(col)
+ if sql not in seen:
+ result.append(sql)
+ seen.add(sql)
+
+ # Still, we need to add all stuff in ordering (except if the backend can
+ # group by just by PK).
+ if ordering_group_by and not self.connection.features.allows_group_by_pk:
+ for order, order_params in ordering_group_by:
+ # Even if we have seen the same SQL string, it might have
+ # different params, so, we add same SQL in "has params" case.
+ if order not in seen or params:
+ result.append(order)
+ params.extend(order_params)
+ seen.add(order)
+
+ # Unconditionally add the extra_select items.
+ for extra_select, extra_params in self.query.extra_select.values():
+ sql = '(%s)' % str(extra_select)
+ result.append(sql)
+ params.extend(extra_params)
+
return result, params
def fill_related_selections(self, opts=None, root_alias=None, cur_depth=1,
- used=None, requested=None, restricted=None, nullable=None,
- dupe_set=None, avoid_set=None):
+ requested=None, restricted=None, nullable=None):
"""
Fill in the information needed for a select_related query. The current
depth is measured as the number of connections away from the root model
@@ -589,14 +602,6 @@ class SQLCompiler(object):
opts = self.query.get_meta()
root_alias = self.query.get_initial_alias()
self.query.related_select_cols = []
- self.query.related_select_fields = []
- if not used:
- used = set()
- if dupe_set is None:
- dupe_set = set()
- if avoid_set is None:
- avoid_set = set()
- orig_dupe_set = dupe_set
only_load = self.query.get_loaded_field_names()
# Setup for the case when only particular related fields should be
@@ -616,12 +621,6 @@ class SQLCompiler(object):
if not select_related_descend(f, restricted, requested,
only_load.get(field_model)):
continue
- # The "avoid" set is aliases we want to avoid just for this
- # particular branch of the recursion. They aren't permanently
- # forbidden from reuse in the related selection tables (which is
- # what "used" specifies).
- avoid = avoid_set.copy()
- dupe_set = orig_dupe_set.copy()
table = f.rel.to._meta.db_table
promote = nullable or f.null
if model:
@@ -637,44 +636,28 @@ class SQLCompiler(object):
int_opts = int_model._meta
continue
lhs_col = int_opts.parents[int_model].column
- dedupe = lhs_col in opts.duplicate_targets
- if dedupe:
- avoid.update(self.query.dupe_avoidance.get((id(opts), lhs_col),
- ()))
- dupe_set.add((opts, lhs_col))
int_opts = int_model._meta
alias = self.query.join((alias, int_opts.db_table, lhs_col,
- int_opts.pk.column), exclusions=used,
+ int_opts.pk.column),
promote=promote)
alias_chain.append(alias)
- for (dupe_opts, dupe_col) in dupe_set:
- self.query.update_dupe_avoidance(dupe_opts, dupe_col, alias)
else:
alias = root_alias
- dedupe = f.column in opts.duplicate_targets
- if dupe_set or dedupe:
- avoid.update(self.query.dupe_avoidance.get((id(opts), f.column), ()))
- if dedupe:
- dupe_set.add((opts, f.column))
-
alias = self.query.join((alias, table, f.column,
f.rel.get_related_field().column),
- exclusions=used.union(avoid), promote=promote)
- used.add(alias)
+ promote=promote, join_field=f)
columns, aliases = self.get_default_columns(start_alias=alias,
opts=f.rel.to._meta, as_pairs=True)
- self.query.related_select_cols.extend(columns)
- self.query.related_select_fields.extend(f.rel.to._meta.fields)
+ self.query.related_select_cols.extend(
+ SelectInfo(col, field) for col, field in zip(columns, f.rel.to._meta.fields))
if restricted:
next = requested.get(f.name, {})
else:
next = False
new_nullable = f.null or promote
- for dupe_opts, dupe_col in dupe_set:
- self.query.update_dupe_avoidance(dupe_opts, dupe_col, alias)
self.fill_related_selections(f.rel.to._meta, alias, cur_depth + 1,
- used, next, restricted, new_nullable, dupe_set, avoid)
+ next, restricted, new_nullable)
if restricted:
related_fields = [
@@ -686,14 +669,8 @@ class SQLCompiler(object):
if not select_related_descend(f, restricted, requested,
only_load.get(model), reverse=True):
continue
- # The "avoid" set is aliases we want to avoid just for this
- # particular branch of the recursion. They aren't permanently
- # forbidden from reuse in the related selection tables (which is
- # what "used" specifies).
- avoid = avoid_set.copy()
- dupe_set = orig_dupe_set.copy()
- table = model._meta.db_table
+ table = model._meta.db_table
int_opts = opts
alias = root_alias
alias_chain = []
@@ -708,42 +685,30 @@ class SQLCompiler(object):
int_opts = int_model._meta
continue
lhs_col = int_opts.parents[int_model].column
- dedupe = lhs_col in opts.duplicate_targets
- if dedupe:
- avoid.update((self.query.dupe_avoidance.get(id(opts), lhs_col),
- ()))
- dupe_set.add((opts, lhs_col))
int_opts = int_model._meta
alias = self.query.join(
(alias, int_opts.db_table, lhs_col, int_opts.pk.column),
- exclusions=used, promote=True, reuse=used
+ promote=True,
)
alias_chain.append(alias)
- for dupe_opts, dupe_col in dupe_set:
- self.query.update_dupe_avoidance(dupe_opts, dupe_col, alias)
- dedupe = f.column in opts.duplicate_targets
- if dupe_set or dedupe:
- avoid.update(self.query.dupe_avoidance.get((id(opts), f.column), ()))
- if dedupe:
- dupe_set.add((opts, f.column))
alias = self.query.join(
(alias, table, f.rel.get_related_field().column, f.column),
- exclusions=used.union(avoid),
- promote=True
+ promote=True, join_field=f
)
- used.add(alias)
+ from_parent = (opts.model if issubclass(model, opts.model)
+ else None)
columns, aliases = self.get_default_columns(start_alias=alias,
- opts=model._meta, as_pairs=True, local_only=True)
- self.query.related_select_cols.extend(columns)
- self.query.related_select_fields.extend(model._meta.fields)
-
+ opts=model._meta, as_pairs=True, from_parent=from_parent)
+ self.query.related_select_cols.extend(
+ SelectInfo(col, field) for col, field
+ in zip(columns, model._meta.fields))
next = requested.get(f.related_query_name(), {})
# Use True here because we are looking at the _reverse_ side of
# the relation, which is always nullable.
new_nullable = True
self.fill_related_selections(model._meta, table, cur_depth+1,
- used, next, restricted, new_nullable)
+ next, restricted, new_nullable)
def deferred_to_columns(self):
"""
@@ -772,7 +737,7 @@ class SQLCompiler(object):
if resolve_columns:
if fields is None:
# We only set this up here because
- # related_select_fields isn't populated until
+ # related_select_cols isn't populated until
# execute_sql() has been called.
# We also include types of fields of related models that
@@ -782,11 +747,11 @@ class SQLCompiler(object):
# This code duplicates the logic for the order of fields
# found in get_columns(). It would be nice to clean this up.
- if self.query.select_fields:
- fields = self.query.select_fields
+ if self.query.select:
+ fields = [f.field for f in self.query.select]
else:
fields = self.query.model._meta.fields
- fields = fields + self.query.related_select_fields
+ fields = fields + [f.field for f in self.query.related_select_cols]
# If the field was deferred, exclude it from being passed
# into `resolve_columns` because it wasn't selected.
@@ -902,6 +867,8 @@ class SQLInsertCompiler(SQLCompiler):
[self.placeholder(field, v) for field, v in zip(fields, val)]
for val in values
]
+ # Oracle Spatial needs to remove some values due to #10888
+ params = self.connection.ops.modify_insert_params(placeholders, params)
if self.return_id and self.connection.features.can_return_id_from_insert:
params = params[0]
col = "%s.%s" % (qn(opts.db_table), qn(opts.pk.column))
diff --git a/django/db/models/sql/constants.py b/django/db/models/sql/constants.py
index f750310624..1c34f70169 100644
--- a/django/db/models/sql/constants.py
+++ b/django/db/models/sql/constants.py
@@ -18,12 +18,22 @@ QUERY_TERMS = set([
# Larger values are slightly faster at the expense of more storage space.
GET_ITERATOR_CHUNK_SIZE = 100
-# Constants to make looking up tuple values clearer.
+# Namedtuples for sql.* internal use.
+
# Join lists (indexes into the tuples that are values in the alias_map
# dictionary in the Query class).
JoinInfo = namedtuple('JoinInfo',
'table_name rhs_alias join_type lhs_alias '
- 'lhs_join_col rhs_join_col nullable')
+ 'lhs_join_col rhs_join_col nullable join_field')
+
+# PathInfo is used when converting lookups (fk__somecol). The contents
+# describe the join in Model terms (model Options and Fields for both
+# sides of the join. The rel_field is the field we are joining along.
+PathInfo = namedtuple('PathInfo',
+ 'from_field to_field from_opts to_opts join_field')
+
+# Pairs of column clauses to select, and (possibly None) field for the clause.
+SelectInfo = namedtuple('SelectInfo', 'col field')
# How many results to expect from a cursor.execute call
MULTI = 'multi'
@@ -34,3 +44,6 @@ ORDER_DIR = {
'ASC': ('ASC', 'DESC'),
'DESC': ('DESC', 'ASC'),
}
+
+# A marker for join-reusability.
+REUSE_ALL = object()
diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py
index 374509914d..af7e45e74e 100644
--- a/django/db/models/sql/expressions.py
+++ b/django/db/models/sql/expressions.py
@@ -1,14 +1,16 @@
from django.core.exceptions import FieldError
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields import FieldDoesNotExist
+from django.db.models.sql.constants import REUSE_ALL
class SQLEvaluator(object):
- def __init__(self, expression, query, allow_joins=True):
+ def __init__(self, expression, query, allow_joins=True, reuse=REUSE_ALL):
self.expression = expression
self.opts = query.get_meta()
self.cols = []
self.contains_aggregate = False
+ self.reuse = reuse
self.expression.prepare(self, query, allow_joins)
def prepare(self):
@@ -48,11 +50,12 @@ class SQLEvaluator(object):
self.cols.append((node, query.aggregate_select[node.name]))
else:
try:
- field, source, opts, join_list, last, _ = query.setup_joins(
+ field, source, opts, join_list, path = query.setup_joins(
field_list, query.get_meta(),
- query.get_initial_alias(), False)
- col, _, join_list = query.trim_joins(source, join_list, last, False)
-
+ query.get_initial_alias(), self.reuse)
+ col, _, join_list = query.trim_joins(source, join_list, path)
+ if self.reuse is not None and self.reuse != REUSE_ALL:
+ self.reuse.update(join_list)
self.cols.append((node, (join_list[-1], col)))
except FieldDoesNotExist:
raise FieldError("Cannot resolve keyword %r into field. "
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index cef01c48ab..ff56211c5d 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -14,13 +14,13 @@ from django.utils.encoding import force_text
from django.utils.tree import Node
from django.utils import six
from django.db import connections, DEFAULT_DB_ALIAS
-from django.db.models import signals
from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import ExpressionNode
from django.db.models.fields import FieldDoesNotExist
+from django.db.models.loading import get_model
from django.db.models.sql import aggregates as base_aggregates_module
from django.db.models.sql.constants import (QUERY_TERMS, ORDER_DIR, SINGLE,
- ORDER_PATTERN, JoinInfo)
+ ORDER_PATTERN, REUSE_ALL, JoinInfo, SelectInfo, PathInfo)
from django.db.models.sql.datastructures import EmptyResultSet, Empty, MultiJoin
from django.db.models.sql.expressions import SQLEvaluator
from django.db.models.sql.where import (WhereNode, Constraint, EverythingNode,
@@ -103,7 +103,7 @@ class Query(object):
def __init__(self, model, where=WhereNode):
self.model = model
- self.alias_refcount = SortedDict()
+ self.alias_refcount = {}
# alias_map is the most important data structure regarding joins.
# It's used for recording which joins exist in the query and what
# type they are. The key is the alias of the joined table (possibly
@@ -115,17 +115,19 @@ class Query(object):
self.default_ordering = True
self.standard_ordering = True
self.ordering_aliases = []
- self.related_select_fields = []
- self.dupe_avoidance = {}
self.used_aliases = set()
self.filter_is_sticky = False
self.included_inherited_models = {}
# SQL-related attributes
+ # Select and related select clauses as SelectInfo instances.
+ # The select is used for cases where we want to set up the select
+ # clause to contain other than default fields (values(), annotate(),
+ # subqueries...)
self.select = []
- # For each to-be-selected field in self.select there must be a
- # corresponding entry in self.select - git seems to need this.
- self.select_fields = []
+ # The related_select_cols is used for columns needed for
+ # select_related - this is populated in compile stage.
+ self.related_select_cols = []
self.tables = [] # Aliases in the order they are created.
self.where = where()
self.where_class = where
@@ -138,7 +140,6 @@ class Query(object):
self.select_for_update = False
self.select_for_update_nowait = False
self.select_related = False
- self.related_select_cols = []
# SQL aggregate-related attributes
self.aggregates = SortedDict() # Maps alias -> SQL aggregate function
@@ -191,16 +192,25 @@ class Query(object):
Pickling support.
"""
obj_dict = self.__dict__.copy()
- obj_dict['related_select_fields'] = []
obj_dict['related_select_cols'] = []
# Fields can't be pickled, so if a field list has been
# specified, we pickle the list of field names instead.
# None is also a possible value; that can pass as-is
- obj_dict['select_fields'] = [
- f is not None and f.name or None
- for f in obj_dict['select_fields']
+ obj_dict['select'] = [
+ (s.col, s.field is not None and s.field.name or None)
+ for s in obj_dict['select']
]
+ # alias_map can also contain references to fields.
+ new_alias_map = {}
+ for alias, join_info in obj_dict['alias_map'].items():
+ if join_info.join_field is None:
+ new_alias_map[alias] = join_info
+ else:
+ model = join_info.join_field.model._meta
+ field_id = (model.app_label, model.object_name, join_info.join_field.name)
+ new_alias_map[alias] = join_info._replace(join_field=field_id)
+ obj_dict['alias_map'] = new_alias_map
return obj_dict
def __setstate__(self, obj_dict):
@@ -209,10 +219,19 @@ class Query(object):
"""
# Rebuild list of field instances
opts = obj_dict['model']._meta
- obj_dict['select_fields'] = [
- name is not None and opts.get_field(name) or None
- for name in obj_dict['select_fields']
+ obj_dict['select'] = [
+ SelectInfo(tpl[0], tpl[1] is not None and opts.get_field(tpl[1]) or None)
+ for tpl in obj_dict['select']
]
+ new_alias_map = {}
+ for alias, join_info in obj_dict['alias_map'].items():
+ if join_info.join_field is None:
+ new_alias_map[alias] = join_info
+ else:
+ field_id = join_info.join_field
+ new_alias_map[alias] = join_info._replace(
+ join_field=get_model(field_id[0], field_id[1])._meta.get_field(field_id[2]))
+ obj_dict['alias_map'] = new_alias_map
self.__dict__.update(obj_dict)
@@ -256,10 +275,8 @@ class Query(object):
obj.standard_ordering = self.standard_ordering
obj.included_inherited_models = self.included_inherited_models.copy()
obj.ordering_aliases = []
- obj.select_fields = self.select_fields[:]
- obj.related_select_fields = self.related_select_fields[:]
- obj.dupe_avoidance = self.dupe_avoidance.copy()
obj.select = self.select[:]
+ obj.related_select_cols = []
obj.tables = self.tables[:]
obj.where = copy.deepcopy(self.where, memo=memo)
obj.where_class = self.where_class
@@ -275,7 +292,6 @@ class Query(object):
obj.select_for_update = self.select_for_update
obj.select_for_update_nowait = self.select_for_update_nowait
obj.select_related = self.select_related
- obj.related_select_cols = []
obj.aggregates = copy.deepcopy(self.aggregates, memo=memo)
if self.aggregate_select_mask is None:
obj.aggregate_select_mask = None
@@ -384,7 +400,6 @@ class Query(object):
query.select_for_update = False
query.select_related = False
query.related_select_cols = []
- query.related_select_fields = []
result = query.get_compiler(using).execute_sql(SINGLE)
if result is None:
@@ -462,24 +477,47 @@ class Query(object):
self.remove_inherited_models()
# Work out how to relabel the rhs aliases, if necessary.
change_map = {}
- used = set()
conjunction = (connector == AND)
- # Add the joins in the rhs query into the new query.
- first = True
- for alias in rhs.tables:
- if not rhs.alias_refcount[alias]:
- # An unused alias.
- continue
- table, _, join_type, lhs, lhs_col, col, _ = rhs.alias_map[alias]
- promote = join_type == self.LOUTER
+
+ # Determine which existing joins can be reused. When combining the
+ # query with AND we must recreate all joins for m2m filters. When
+ # combining with OR we can reuse joins. The reason is that in AND
+ # case a single row can't fulfill a condition like:
+ # revrel__col=1 & revrel__col=2
+ # But, there might be two different related rows matching this
+ # condition. In OR case a single True is enough, so single row is
+ # enough, too.
+ #
+ # Note that we will be creating duplicate joins for non-m2m joins in
+ # the AND case. The results will be correct but this creates too many
+ # joins. This is something that could be fixed later on.
+ reuse = set() if conjunction else set(self.tables)
+ # Base table must be present in the query - this is the same
+ # table on both sides.
+ self.get_initial_alias()
+ # Now, add the joins from rhs query into the new query (skipping base
+ # table).
+ for alias in rhs.tables[1:]:
+ table, _, join_type, lhs, lhs_col, col, nullable, join_field = rhs.alias_map[alias]
+ promote = (join_type == self.LOUTER)
# If the left side of the join was already relabeled, use the
# updated alias.
lhs = change_map.get(lhs, lhs)
- new_alias = self.join((lhs, table, lhs_col, col),
- conjunction and not first, used, promote, not conjunction)
- used.add(new_alias)
+ new_alias = self.join(
+ (lhs, table, lhs_col, col), reuse=reuse, promote=promote,
+ outer_if_first=not conjunction, nullable=nullable,
+ join_field=join_field)
+ # We can't reuse the same join again in the query. If we have two
+ # distinct joins for the same connection in rhs query, then the
+ # combined query must have two joins, too.
+ reuse.discard(new_alias)
change_map[alias] = new_alias
- first = False
+ if not rhs.alias_refcount[alias]:
+ # The alias was unused in the rhs query. Unref it so that it
+ # will be unused in the new query, too. We have to add and
+ # unref the alias so that join promotion has information of
+ # the join type for the unused alias.
+ self.unref_alias(new_alias)
# So that we don't exclude valid results in an "or" query combination,
# all joins exclusive to either the lhs or the rhs must be converted
@@ -527,14 +565,14 @@ class Query(object):
# Selection columns and extra extensions are those provided by 'rhs'.
self.select = []
- for col in rhs.select:
+ for col, field in rhs.select:
if isinstance(col, (list, tuple)):
- self.select.append((change_map.get(col[0], col[0]), col[1]))
+ new_col = change_map.get(col[0], col[0]), col[1]
+ self.select.append(SelectInfo(new_col, field))
else:
item = copy.deepcopy(col)
item.relabel_aliases(change_map)
- self.select.append(item)
- self.select_fields = rhs.select_fields[:]
+ self.select.append(SelectInfo(item, field))
if connector == OR:
# It would be nice to be able to handle this, but the queries don't
@@ -585,17 +623,22 @@ class Query(object):
for name in parts[:-1]:
old_model = cur_model
source = opts.get_field_by_name(name)[0]
- cur_model = source.rel.to
+ if is_reverse_o2o(source):
+ cur_model = source.model
+ else:
+ cur_model = source.rel.to
opts = cur_model._meta
# Even if we're "just passing through" this model, we must add
# both the current model's pk and the related reference field
- # to the things we select.
- must_include[old_model].add(source)
+ # (if it's not a reverse relation) to the things we select.
+ if not is_reverse_o2o(source):
+ must_include[old_model].add(source)
add_to_dict(must_include, cur_model, opts.pk)
field, model, _, _ = opts.get_field_by_name(parts[-1])
if model is None:
model = cur_model
- add_to_dict(seen, model, field)
+ if not is_reverse_o2o(field):
+ add_to_dict(seen, model, field)
if defer:
# We need to load all fields for each model, except those that
@@ -750,29 +793,30 @@ class Query(object):
"""
assert set(change_map.keys()).intersection(set(change_map.values())) == set()
+ def relabel_column(col):
+ if isinstance(col, (list, tuple)):
+ old_alias = col[0]
+ return (change_map.get(old_alias, old_alias), col[1])
+ else:
+ col.relabel_aliases(change_map)
+ return col
# 1. Update references in "select" (normal columns plus aliases),
# "group by", "where" and "having".
self.where.relabel_aliases(change_map)
self.having.relabel_aliases(change_map)
- for columns in [self.select, self.group_by or []]:
- for pos, col in enumerate(columns):
- if isinstance(col, (list, tuple)):
- old_alias = col[0]
- columns[pos] = (change_map.get(old_alias, old_alias), col[1])
- else:
- col.relabel_aliases(change_map)
- for mapping in [self.aggregates]:
- for key, col in mapping.items():
- if isinstance(col, (list, tuple)):
- old_alias = col[0]
- mapping[key] = (change_map.get(old_alias, old_alias), col[1])
- else:
- col.relabel_aliases(change_map)
+ if self.group_by:
+ self.group_by = [relabel_column(col) for col in self.group_by]
+ self.select = [SelectInfo(relabel_column(s.col), s.field)
+ for s in self.select]
+ self.aggregates = SortedDict(
+ (key, relabel_column(col)) for key, col in self.aggregates.items())
# 2. Rename the alias in the internal table/alias datastructures.
- for k, aliases in self.join_map.items():
+ for ident, aliases in self.join_map.items():
+ del self.join_map[ident]
aliases = tuple([change_map.get(a, a) for a in aliases])
- self.join_map[k] = aliases
+ ident = (change_map.get(ident[0], ident[0]),) + ident[1:]
+ self.join_map[ident] = aliases
for old_alias, new_alias in six.iteritems(change_map):
alias_data = self.alias_map[old_alias]
alias_data = alias_data._replace(rhs_alias=new_alias)
@@ -845,10 +889,10 @@ class Query(object):
count. Note that after execution, the reference counts are zeroed, so
tables added in compiler will not be seen by this method.
"""
- return len([1 for count in six.itervalues(self.alias_refcount) if count])
+ return len([1 for count in self.alias_refcount.values() if count])
- def join(self, connection, always_create=False, exclusions=(),
- promote=False, outer_if_first=False, nullable=False, reuse=None):
+ def join(self, connection, reuse=REUSE_ALL, promote=False,
+ outer_if_first=False, nullable=False, join_field=None):
"""
Returns an alias for the join in 'connection', either reusing an
existing alias for that join or creating a new one. 'connection' is a
@@ -858,56 +902,47 @@ class Query(object):
lhs.lhs_col = table.col
- If 'always_create' is True and 'reuse' is None, a new alias is always
- created, regardless of whether one already exists or not. If
- 'always_create' is True and 'reuse' is a set, an alias in 'reuse' that
- matches the connection will be returned, if possible. If
- 'always_create' is False, the first existing alias that matches the
- 'connection' is returned, if any. Otherwise a new join is created.
-
- If 'exclusions' is specified, it is something satisfying the container
- protocol ("foo in exclusions" must work) and specifies a list of
- aliases that should not be returned, even if they satisfy the join.
+ The 'reuse' parameter can be used in three ways: it can be REUSE_ALL
+ which means all joins (matching the connection) are reusable, it can
+ be a set containing the aliases that can be reused, or it can be None
+ which means a new join is always created.
If 'promote' is True, the join type for the alias will be LOUTER (if
the alias previously existed, the join type will be promoted from INNER
to LOUTER, if necessary).
If 'outer_if_first' is True and a new join is created, it will have the
- LOUTER join type. This is used when joining certain types of querysets
- and Q-objects together.
+ LOUTER join type. Used for example when adding ORed filters, where we
+ want to use LOUTER joins except if some other join already restricts
+ the join to INNER join.
A join is always created as LOUTER if the lhs alias is LOUTER to make
- sure we do not generate chains like a LOUTER b INNER c.
+ sure we do not generate chains like t1 LOUTER t2 INNER t3.
If 'nullable' is True, the join can potentially involve NULL values and
is a candidate for promotion (to "left outer") when combining querysets.
+
+ The 'join_field' is the field we are joining along (if any).
"""
lhs, table, lhs_col, col = connection
- if lhs in self.alias_map:
- lhs_table = self.alias_map[lhs].table_name
+ existing = self.join_map.get(connection, ())
+ if reuse == REUSE_ALL:
+ reuse = existing
+ elif reuse is None:
+ reuse = set()
else:
- lhs_table = lhs
-
- if reuse and always_create and table in self.table_map:
- # Convert the 'reuse' to case to be "exclude everything but the
- # reusable set, minus exclusions, for this table".
- exclusions = set(self.table_map[table]).difference(reuse).union(set(exclusions))
- always_create = False
- t_ident = (lhs_table, table, lhs_col, col)
- if not always_create:
- for alias in self.join_map.get(t_ident, ()):
- if alias not in exclusions:
- if lhs_table and not self.alias_refcount[self.alias_map[alias].lhs_alias]:
- # The LHS of this join tuple is no longer part of the
- # query, so skip this possibility.
- continue
- if self.alias_map[alias].lhs_alias != lhs:
- continue
- self.ref_alias(alias)
- if promote or (lhs and self.alias_map[lhs].join_type == self.LOUTER):
- self.promote_joins([alias])
- return alias
+ reuse = [a for a in existing if a in reuse]
+ for alias in reuse:
+ if join_field and self.alias_map[alias].join_field != join_field:
+ # The join_map doesn't contain join_field (mainly because
+ # fields in Query structs are problematic in pickling), so
+ # check that the existing join is created using the same
+ # join_field used for the under work join.
+ continue
+ self.ref_alias(alias)
+ if promote or (lhs and self.alias_map[lhs].join_type == self.LOUTER):
+ self.promote_joins([alias])
+ return alias
# No reuse is possible, so we need a new alias.
alias, _ = self.table_alias(table, True)
@@ -918,18 +953,17 @@ class Query(object):
elif (promote or outer_if_first
or self.alias_map[lhs].join_type == self.LOUTER):
# We need to use LOUTER join if asked by promote or outer_if_first,
- # or if the LHS table is left-joined in the query. Adding inner join
- # to an existing outer join effectively cancels the effect of the
- # outer join.
+ # or if the LHS table is left-joined in the query.
join_type = self.LOUTER
else:
join_type = self.INNER
- join = JoinInfo(table, alias, join_type, lhs, lhs_col, col, nullable)
+ join = JoinInfo(table, alias, join_type, lhs, lhs_col, col, nullable,
+ join_field)
self.alias_map[alias] = join
- if t_ident in self.join_map:
- self.join_map[t_ident] += (alias,)
+ if connection in self.join_map:
+ self.join_map[connection] += (alias,)
else:
- self.join_map[t_ident] = (alias,)
+ self.join_map[connection] = (alias,)
return alias
def setup_inherited_models(self):
@@ -1005,11 +1039,11 @@ class Query(object):
# - this is an annotation over a model field
# then we need to explore the joins that are required.
- field, source, opts, join_list, last, _ = self.setup_joins(
- field_list, opts, self.get_initial_alias(), False)
+ field, source, opts, join_list, path = self.setup_joins(
+ field_list, opts, self.get_initial_alias(), REUSE_ALL)
# Process the join chain to see if it can be trimmed
- col, _, join_list = self.trim_joins(source, join_list, last, False)
+ col, _, join_list = self.trim_joins(source, join_list, path)
# If the aggregate references a model or field that requires a join,
# those joins must be LEFT OUTER - empty join rows must be returned
@@ -1027,8 +1061,8 @@ class Query(object):
# Add the aggregate to the query
aggregate.add_to_query(self, alias, col=col, source=source, is_summary=is_summary)
- def add_filter(self, filter_expr, connector=AND, negate=False, trim=False,
- can_reuse=None, process_extras=True, force_having=False):
+ def add_filter(self, filter_expr, connector=AND, negate=False,
+ can_reuse=None, force_having=False):
"""
Add a single filter to the query. The 'filter_expr' is a pair:
(filter_string, value). E.g. ('name__contains', 'fred')
@@ -1040,18 +1074,11 @@ class Query(object):
should only happen once. So the caller is responsible for this (the
caller will normally be add_q(), so that as an example).
- If 'trim' is True, we automatically trim the final join group (used
- internally when constructing nested queries).
-
If 'can_reuse' is a set, we are processing a component of a
multi-component filter (e.g. filter(Q1, Q2)). In this case, 'can_reuse'
will be a set of table aliases that can be reused in this filter, even
if we would otherwise force the creation of new aliases for a join
(needed for nested Q-filters). The set is updated by this method.
-
- If 'process_extras' is set, any extra filters returned from the table
- joining process will be processed. This parameter is set to False
- during the processing of extra filters to avoid infinite recursion.
"""
arg, value = filter_expr
parts = arg.split(LOOKUP_SEP)
@@ -1099,7 +1126,7 @@ class Query(object):
value = value()
elif isinstance(value, ExpressionNode):
# If value is a query expression, evaluate it
- value = SQLEvaluator(value, self)
+ value = SQLEvaluator(value, self, reuse=can_reuse)
having_clause = value.contains_aggregate
for alias, aggregate in self.aggregates.items():
@@ -1113,13 +1140,14 @@ class Query(object):
opts = self.get_meta()
alias = self.get_initial_alias()
- allow_many = trim or not negate
+ allow_many = not negate
try:
- field, target, opts, join_list, last, extra_filters = self.setup_joins(
- parts, opts, alias, True, allow_many, allow_explicit_fk=True,
- can_reuse=can_reuse, negate=negate,
- process_extras=process_extras)
+ field, target, opts, join_list, path = self.setup_joins(
+ parts, opts, alias, can_reuse, allow_many,
+ allow_explicit_fk=True)
+ if can_reuse is not None:
+ can_reuse.update(join_list)
except MultiJoin as e:
self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:e.level]),
can_reuse)
@@ -1137,10 +1165,10 @@ class Query(object):
join_promote = True
# Process the join list to see if we can remove any inner joins from
- # the far end (fewer tables in a query is better).
- nonnull_comparison = (lookup_type == 'isnull' and value is False)
- col, alias, join_list = self.trim_joins(target, join_list, last, trim,
- nonnull_comparison)
+ # the far end (fewer tables in a query is better). Note that join
+ # promotion must happen before join trimming to have the join type
+ # information available when reusing joins.
+ col, alias, join_list = self.trim_joins(target, join_list, path)
if connector == OR:
# Some joins may need to be promoted when adding a new filter to a
@@ -1213,12 +1241,6 @@ class Query(object):
# is added in upper layers of the code.
self.where.add((Constraint(alias, col, None), 'isnull', False), AND)
- if can_reuse is not None:
- can_reuse.update(join_list)
- if process_extras:
- for filter in extra_filters:
- self.add_filter(filter, negate=negate, can_reuse=can_reuse,
- process_extras=False)
def add_q(self, q_object, used_aliases=None, force_having=False):
"""
@@ -1271,37 +1293,24 @@ class Query(object):
if self.filter_is_sticky:
self.used_aliases = used_aliases
- def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True,
- allow_explicit_fk=False, can_reuse=None, negate=False,
- process_extras=True):
+ def names_to_path(self, names, opts, allow_many=False,
+ allow_explicit_fk=True):
"""
- Compute the necessary table joins for the passage through the fields
- given in 'names'. 'opts' is the Options class for the current model
- (which gives the table we are joining to), 'alias' is the alias for the
- table we are joining to. If dupe_multis is True, any many-to-many or
- many-to-one joins will always create a new alias (necessary for
- disjunctive filters). If can_reuse is not None, it's a list of aliases
- that can be reused in these joins (nothing else can be reused in this
- case). Finally, 'negate' is used in the same sense as for add_filter()
- -- it indicates an exclude() filter, or something similar. It is only
- passed in here so that it can be passed to a field's extra_filter() for
- customized behavior.
+ Walks the names path and turns them PathInfo tuples. Note that a
+ single name in 'names' can generate multiple PathInfos (m2m for
+ example).
+
+ 'names' is the path of names to travle, 'opts' is the model Options we
+ start the name resolving from, 'allow_many' and 'allow_explicit_fk'
+ are as for setup_joins().
- Returns the final field involved in the join, the target database
- column (used for any 'where' constraint), the final 'opts' value and the
- list of tables joined.
+ Returns a list of PathInfo tuples. In addition returns the final field
+ (the last used join field), and target (which is a field guaranteed to
+ contain the same value as the final field).
"""
- joins = [alias]
- last = [0]
- dupe_set = set()
- exclusions = set()
- extra_filters = []
- int_alias = None
+ path = []
+ multijoin_pos = None
for pos, name in enumerate(names):
- if int_alias is not None:
- exclusions.add(int_alias)
- exclusions.add(alias)
- last.append(len(joins))
if name == 'pk':
name = opts.pk.name
try:
@@ -1315,14 +1324,12 @@ class Query(object):
field, model, direct, m2m = opts.get_field_by_name(f.name)
break
else:
- names = opts.get_all_field_names() + list(self.aggregate_select)
+ available = opts.get_all_field_names() + list(self.aggregate_select)
raise FieldError("Cannot resolve keyword %r into field. "
- "Choices are: %s" % (name, ", ".join(names)))
-
- if not allow_many and (m2m or not direct):
- for alias in joins:
- self.unref_alias(alias)
- raise MultiJoin(pos + 1)
+ "Choices are: %s" % (name, ", ".join(available)))
+ # Check if we need any joins for concrete inheritance cases (the
+ # field lives in parent, but we are currently in one of its
+ # children)
if model:
# The field lives on a base class of the current model.
# Skip the chain of proxy to the concrete proxied model
@@ -1332,227 +1339,179 @@ class Query(object):
if int_model is proxied_model:
opts = int_model._meta
else:
- lhs_col = opts.parents[int_model].column
- dedupe = lhs_col in opts.duplicate_targets
- if dedupe:
- exclusions.update(self.dupe_avoidance.get(
- (id(opts), lhs_col), ()))
- dupe_set.add((opts, lhs_col))
+ final_field = opts.parents[int_model]
+ target = final_field.rel.get_related_field()
opts = int_model._meta
- alias = self.join((alias, opts.db_table, lhs_col,
- opts.pk.column), exclusions=exclusions)
- joins.append(alias)
- exclusions.add(alias)
- for (dupe_opts, dupe_col) in dupe_set:
- self.update_dupe_avoidance(dupe_opts, dupe_col,
- alias)
- cached_data = opts._join_cache.get(name)
- orig_opts = opts
- dupe_col = direct and field.column or field.field.column
- dedupe = dupe_col in opts.duplicate_targets
- if dupe_set or dedupe:
- if dedupe:
- dupe_set.add((opts, dupe_col))
- exclusions.update(self.dupe_avoidance.get((id(opts), dupe_col),
- ()))
-
- if process_extras and hasattr(field, 'extra_filters'):
- extra_filters.extend(field.extra_filters(names, pos, negate))
- if direct:
- if m2m:
- # Many-to-many field defined on the current model.
- if cached_data:
- (table1, from_col1, to_col1, table2, from_col2,
- to_col2, opts, target) = cached_data
- else:
- table1 = field.m2m_db_table()
- from_col1 = opts.get_field_by_name(
- field.m2m_target_field_name())[0].column
- to_col1 = field.m2m_column_name()
- opts = field.rel.to._meta
- table2 = opts.db_table
- from_col2 = field.m2m_reverse_name()
- to_col2 = opts.get_field_by_name(
- field.m2m_reverse_target_field_name())[0].column
- target = opts.pk
- orig_opts._join_cache[name] = (table1, from_col1,
- to_col1, table2, from_col2, to_col2, opts,
- target)
-
- int_alias = self.join((alias, table1, from_col1, to_col1),
- dupe_multis, exclusions, nullable=True,
- reuse=can_reuse)
- if int_alias == table2 and from_col2 == to_col2:
- joins.append(int_alias)
- alias = int_alias
- else:
- alias = self.join(
- (int_alias, table2, from_col2, to_col2),
- dupe_multis, exclusions, nullable=True,
- reuse=can_reuse)
- joins.extend([int_alias, alias])
- elif field.rel:
- # One-to-one or many-to-one field
- if cached_data:
- (table, from_col, to_col, opts, target) = cached_data
- else:
- opts = field.rel.to._meta
- target = field.rel.get_related_field()
- table = opts.db_table
- from_col = field.column
- to_col = target.column
- orig_opts._join_cache[name] = (table, from_col, to_col,
- opts, target)
-
- alias = self.join((alias, table, from_col, to_col),
- exclusions=exclusions,
- nullable=self.is_nullable(field))
- joins.append(alias)
+ path.append(PathInfo(final_field, target, final_field.model._meta,
+ opts, final_field))
+ # We have five different cases to solve: foreign keys, reverse
+ # foreign keys, m2m fields (also reverse) and non-relational
+ # fields. We are mostly just using the related field API to
+ # fetch the from and to fields. The m2m fields are handled as
+ # two foreign keys, first one reverse, the second one direct.
+ if direct and not field.rel and not m2m:
+ # Local non-relational field.
+ final_field = target = field
+ break
+ elif direct and not m2m:
+ # Foreign Key
+ opts = field.rel.to._meta
+ target = field.rel.get_related_field()
+ final_field = field
+ from_opts = field.model._meta
+ path.append(PathInfo(field, target, from_opts, opts, field))
+ elif not direct and not m2m:
+ # Revere foreign key
+ final_field = to_field = field.field
+ opts = to_field.model._meta
+ from_field = to_field.rel.get_related_field()
+ from_opts = from_field.model._meta
+ path.append(
+ PathInfo(from_field, to_field, from_opts, opts, to_field))
+ if from_field.model is to_field.model:
+ # Recursive foreign key to self.
+ target = opts.get_field_by_name(
+ field.field.rel.field_name)[0]
else:
- # Non-relation fields.
- target = field
- break
- else:
- orig_field = field
- field = field.field
- if m2m:
- # Many-to-many field defined on the target model.
- if cached_data:
- (table1, from_col1, to_col1, table2, from_col2,
- to_col2, opts, target) = cached_data
- else:
- table1 = field.m2m_db_table()
- from_col1 = opts.get_field_by_name(
- field.m2m_reverse_target_field_name())[0].column
- to_col1 = field.m2m_reverse_name()
- opts = orig_field.opts
- table2 = opts.db_table
- from_col2 = field.m2m_column_name()
- to_col2 = opts.get_field_by_name(
- field.m2m_target_field_name())[0].column
- target = opts.pk
- orig_opts._join_cache[name] = (table1, from_col1,
- to_col1, table2, from_col2, to_col2, opts,
- target)
-
- int_alias = self.join((alias, table1, from_col1, to_col1),
- dupe_multis, exclusions, nullable=True,
- reuse=can_reuse)
- alias = self.join((int_alias, table2, from_col2, to_col2),
- dupe_multis, exclusions, nullable=True,
- reuse=can_reuse)
- joins.extend([int_alias, alias])
+ target = opts.pk
+ elif direct and m2m:
+ if not field.rel.through:
+ # Gotcha! This is just a fake m2m field - a generic relation
+ # field).
+ from_field = opts.pk
+ opts = field.rel.to._meta
+ target = opts.get_field_by_name(field.object_id_field_name)[0]
+ final_field = field
+ # Note that we are using different field for the join_field
+ # than from_field or to_field. This is a hack, but we need the
+ # GenericRelation to generate the extra SQL.
+ path.append(PathInfo(from_field, target, field.model._meta, opts,
+ field))
else:
- # One-to-many field (ForeignKey defined on the target model)
- if cached_data:
- (table, from_col, to_col, opts, target) = cached_data
- else:
- local_field = opts.get_field_by_name(
- field.rel.field_name)[0]
- opts = orig_field.opts
- table = opts.db_table
- from_col = local_field.column
- to_col = field.column
- # In case of a recursive FK, use the to_field for
- # reverse lookups as well
- if orig_field.model is local_field.model:
- target = opts.get_field_by_name(
- field.rel.field_name)[0]
- else:
- target = opts.pk
- orig_opts._join_cache[name] = (table, from_col, to_col,
- opts, target)
-
- alias = self.join((alias, table, from_col, to_col),
- dupe_multis, exclusions, nullable=True,
- reuse=can_reuse)
- joins.append(alias)
+ # m2m field. We are travelling first to the m2m table along a
+ # reverse relation, then from m2m table to the target table.
+ from_field1 = opts.get_field_by_name(
+ field.m2m_target_field_name())[0]
+ opts = field.rel.through._meta
+ to_field1 = opts.get_field_by_name(field.m2m_field_name())[0]
+ path.append(
+ PathInfo(from_field1, to_field1, from_field1.model._meta,
+ opts, to_field1))
+ final_field = from_field2 = opts.get_field_by_name(
+ field.m2m_reverse_field_name())[0]
+ opts = field.rel.to._meta
+ target = to_field2 = opts.get_field_by_name(
+ field.m2m_reverse_target_field_name())[0]
+ path.append(
+ PathInfo(from_field2, to_field2, from_field2.model._meta,
+ opts, from_field2))
+ elif not direct and m2m:
+ # This one is just like above, except we are travelling the
+ # fields in opposite direction.
+ field = field.field
+ from_field1 = opts.get_field_by_name(
+ field.m2m_reverse_target_field_name())[0]
+ int_opts = field.rel.through._meta
+ to_field1 = int_opts.get_field_by_name(
+ field.m2m_reverse_field_name())[0]
+ path.append(
+ PathInfo(from_field1, to_field1, from_field1.model._meta,
+ int_opts, to_field1))
+ final_field = from_field2 = int_opts.get_field_by_name(
+ field.m2m_field_name())[0]
+ opts = field.opts
+ target = to_field2 = opts.get_field_by_name(
+ field.m2m_target_field_name())[0]
+ path.append(PathInfo(from_field2, to_field2, from_field2.model._meta,
+ opts, from_field2))
- for (dupe_opts, dupe_col) in dupe_set:
- if int_alias is None:
- to_avoid = alias
- else:
- to_avoid = int_alias
- self.update_dupe_avoidance(dupe_opts, dupe_col, to_avoid)
+ if m2m and multijoin_pos is None:
+ multijoin_pos = pos
+ if not direct and not path[-1].to_field.unique and multijoin_pos is None:
+ multijoin_pos = pos
if pos != len(names) - 1:
if pos == len(names) - 2:
- raise FieldError("Join on field %r not permitted. Did you misspell %r for the lookup type?" % (name, names[pos + 1]))
+ raise FieldError(
+ "Join on field %r not permitted. Did you misspell %r for "
+ "the lookup type?" % (name, names[pos + 1]))
else:
raise FieldError("Join on field %r not permitted." % name)
+ if multijoin_pos is not None and len(path) >= multijoin_pos and not allow_many:
+ raise MultiJoin(multijoin_pos + 1)
+ return path, final_field, target
- return field, target, opts, joins, last, extra_filters
-
- def trim_joins(self, target, join_list, last, trim, nonnull_check=False):
+ def setup_joins(self, names, opts, alias, can_reuse, allow_many=True,
+ allow_explicit_fk=False):
"""
- Sometimes joins at the end of a multi-table sequence can be trimmed. If
- the final join is against the same column as we are comparing against,
- and is an inner join, we can go back one step in a join chain and
- compare against the LHS of the join instead (and then repeat the
- optimization). The result, potentially, involves fewer table joins.
+ Compute the necessary table joins for the passage through the fields
+ given in 'names'. 'opts' is the Options class for the current model
+ (which gives the table we are starting from), 'alias' is the alias for
+ the table to start the joining from.
- The 'target' parameter is the final field being joined to, 'join_list'
- is the full list of join aliases.
+ The 'can_reuse' defines the reverse foreign key joins we can reuse. It
+ can be sql.constants.REUSE_ALL in which case all joins are reusable
+ or a set of aliases that can be reused. Note that Non-reverse foreign
+ keys are always reusable.
- The 'last' list contains offsets into 'join_list', corresponding to
- each component of the filter. Many-to-many relations, for example, add
- two tables to the join list and we want to deal with both tables the
- same way, so 'last' has an entry for the first of the two tables and
- then the table immediately after the second table, in that case.
+ If 'allow_many' is False, then any reverse foreign key seen will
+ generate a MultiJoin exception.
- The 'trim' parameter forces the final piece of the join list to be
- trimmed before anything. See the documentation of add_filter() for
- details about this.
+ The 'allow_explicit_fk' controls if field.attname is allowed in the
+ lookups.
- The 'nonnull_check' parameter is True when we are using inner joins
- between tables explicitly to exclude NULL entries. In that case, the
- tables shouldn't be trimmed, because the very action of joining to them
- alters the result set.
+ Returns the final field involved in the joins, the target field (used
+ for any 'where' constraint), the final 'opts' value, the joins and the
+ field path travelled to generate the joins.
- Returns the final active column and table alias and the new active
- join_list.
+ The target field is the field containing the concrete value. Final
+ field can be something different, for example foreign key pointing to
+ that value. Final field is needed for example in some value
+ conversions (convert 'obj' in fk__id=obj to pk val using the foreign
+ key field for example).
"""
- final = len(join_list)
- penultimate = last.pop()
- if penultimate == final:
- penultimate = last.pop()
- if trim and final > 1:
- extra = join_list[penultimate:]
- join_list = join_list[:penultimate]
- final = penultimate
- penultimate = last.pop()
- col = self.alias_map[extra[0]].lhs_join_col
- for alias in extra:
- self.unref_alias(alias)
- else:
- col = target.column
- alias = join_list[-1]
- while final > 1:
- join = self.alias_map[alias]
- if (col != join.rhs_join_col or join.join_type != self.INNER or
- nonnull_check):
- break
- self.unref_alias(alias)
- alias = join.lhs_alias
- col = join.lhs_join_col
- join_list.pop()
- final -= 1
- if final == penultimate:
- penultimate = last.pop()
- return col, alias, join_list
+ joins = [alias]
+ # First, generate the path for the names
+ path, final_field, target = self.names_to_path(
+ names, opts, allow_many, allow_explicit_fk)
+ # Then, add the path to the query's joins. Note that we can't trim
+ # joins at this stage - we will need the information about join type
+ # of the trimmed joins.
+ for pos, join in enumerate(path):
+ from_field, to_field, from_opts, opts, join_field = join
+ direct = join_field == from_field
+ if direct:
+ nullable = self.is_nullable(from_field)
+ else:
+ nullable = True
+ connection = alias, opts.db_table, from_field.column, to_field.column
+ alias = self.join(connection, reuse=can_reuse, nullable=nullable,
+ join_field=join_field)
+ joins.append(alias)
+ return final_field, target, opts, joins, path
- def update_dupe_avoidance(self, opts, col, alias):
+ def trim_joins(self, target, joins, path):
"""
- For a column that is one of multiple pointing to the same table, update
- the internal data structures to note that this alias shouldn't be used
- for those other columns.
+ The 'target' parameter is the final field being joined to, 'joins'
+ is the full list of join aliases. The 'path' contain the PathInfos
+ used to create the joins.
+
+ Returns the final active column and table alias and the new active
+ joins.
+
+ We will always trim any direct join if we have the target column
+ available already in the previous table. Reverse joins can't be
+ trimmed as we don't know if there is anything on the other side of
+ the join.
"""
- ident = id(opts)
- for name in opts.duplicate_targets[col]:
- try:
- self.dupe_avoidance[ident, name].add(alias)
- except KeyError:
- self.dupe_avoidance[ident, name] = set([alias])
+ for info in reversed(path):
+ direct = info.join_field == info.from_field
+ if info.to_field == target and direct:
+ target = info.from_field
+ self.unref_alias(joins.pop())
+ else:
+ break
+ return target.column, joins[-1], joins
def split_exclude(self, filter_expr, prefix, can_reuse):
"""
@@ -1560,6 +1519,19 @@ class Query(object):
to use a subquery. This method constructs the nested query, given the
original exclude filter (filter_expr) and the portion up to the first
N-to-many relation field.
+
+ As an example we could have original filter ~Q(child__name='foo').
+ We would get here with filter_expr = child_name, prefix = child and
+ can_reuse is a set of joins we can reuse for filtering in the original
+ query.
+
+ We will turn this into
+ WHERE pk NOT IN (SELECT parent_id FROM thetable
+ WHERE name = 'foo' AND parent_id IS NOT NULL)
+
+ It might be worth it to consider using WHERE NOT EXISTS as that has
+ saner null handling, and is easier for the backend's optimizer to
+ handle.
"""
query = Query(self.model)
query.add_filter(filter_expr)
@@ -1570,10 +1542,21 @@ class Query(object):
# since we are adding a IN <subquery> clause. This prevents the
# database from tripping over IN (...,NULL,...) selects and returning
# nothing
- alias, col = query.select[0]
+ alias, col = query.select[0].col
query.where.add((Constraint(alias, col, None), 'isnull', False), AND)
+ # We need to trim the last part from the prefix.
+ trimmed_prefix = LOOKUP_SEP.join(prefix.split(LOOKUP_SEP)[0:-1])
+ if not trimmed_prefix:
+ rel, _, direct, m2m = self.model._meta.get_field_by_name(prefix)
+ if not m2m:
+ trimmed_prefix = rel.field.rel.field_name
+ else:
+ if direct:
+ trimmed_prefix = rel.m2m_target_field_name()
+ else:
+ trimmed_prefix = rel.field.m2m_reverse_target_field_name()
- self.add_filter(('%s__in' % prefix, query), negate=True, trim=True,
+ self.add_filter(('%s__in' % trimmed_prefix, query), negate=True,
can_reuse=can_reuse)
# If there's more than one join in the inner query (before any initial
@@ -1583,11 +1566,11 @@ class Query(object):
# comparison to NULL (e.g. in
# Tag.objects.exclude(parent__parent__name='t1'), a tag with no parent
# would otherwise be overlooked).
- active_positions = [pos for (pos, count) in
- enumerate(six.itervalues(query.alias_refcount)) if count]
- if active_positions[-1] > 1:
- self.add_filter(('%s__isnull' % prefix, False), negate=True,
- trim=True, can_reuse=can_reuse)
+ active_positions = len([count for count
+ in query.alias_refcount.items() if count])
+ if active_positions > 1:
+ self.add_filter(('%s__isnull' % trimmed_prefix, False), negate=True,
+ can_reuse=can_reuse)
def set_limits(self, low=None, high=None):
"""
@@ -1629,7 +1612,6 @@ class Query(object):
Removes all fields from SELECT clause.
"""
self.select = []
- self.select_fields = []
self.default_cols = False
self.select_related = False
self.set_extra_mask(())
@@ -1642,7 +1624,6 @@ class Query(object):
columns.
"""
self.select = []
- self.select_fields = []
def add_distinct_fields(self, *field_names):
"""
@@ -1661,8 +1642,8 @@ class Query(object):
try:
for name in field_names:
- field, target, u2, joins, u3, u4 = self.setup_joins(
- name.split(LOOKUP_SEP), opts, alias, False, allow_m2m,
+ field, target, u2, joins, u3 = self.setup_joins(
+ name.split(LOOKUP_SEP), opts, alias, REUSE_ALL, allow_m2m,
True)
final_alias = joins[-1]
col = target.column
@@ -1674,8 +1655,7 @@ class Query(object):
col = join.lhs_join_col
joins = joins[:-1]
self.promote_joins(joins[1:])
- self.select.append((final_alias, col))
- self.select_fields.append(field)
+ self.select.append(SelectInfo((final_alias, col), field))
except MultiJoin:
raise FieldError("Invalid field name: '%s'" % name)
except FieldError:
@@ -1731,8 +1711,8 @@ class Query(object):
"""
self.group_by = []
- for sel in self.select:
- self.group_by.append(sel)
+ for col, _ in self.select:
+ self.group_by.append(col)
def add_count_column(self):
"""
@@ -1745,7 +1725,7 @@ class Query(object):
else:
assert len(self.select) == 1, \
"Cannot add count col with multiple cols in 'select': %r" % self.select
- count = self.aggregates_module.Count(self.select[0])
+ count = self.aggregates_module.Count(self.select[0].col)
else:
opts = self.model._meta
if not self.select:
@@ -1757,7 +1737,7 @@ class Query(object):
assert len(self.select) == 1, \
"Cannot add count col with multiple cols in 'select'."
- count = self.aggregates_module.Count(self.select[0], distinct=True)
+ count = self.aggregates_module.Count(self.select[0].col, distinct=True)
# Distinct handling is done in Count(), so don't do it at this
# level.
self.distinct = False
@@ -1781,7 +1761,6 @@ class Query(object):
d = d.setdefault(part, {})
self.select_related = field_dict
self.related_select_cols = []
- self.related_select_fields = []
def add_extra(self, select, select_params, where, params, tables, order_by):
"""
@@ -1954,8 +1933,8 @@ class Query(object):
"""
opts = self.model._meta
alias = self.get_initial_alias()
- field, col, opts, joins, last, extra = self.setup_joins(
- start.split(LOOKUP_SEP), opts, alias, False)
+ field, col, opts, joins, extra = self.setup_joins(
+ start.split(LOOKUP_SEP), opts, alias, REUSE_ALL)
select_col = self.alias_map[joins[1]].lhs_join_col
select_alias = alias
@@ -1975,7 +1954,7 @@ class Query(object):
self.unref_alias(select_alias)
select_alias = join_info.rhs_alias
select_col = join_info.rhs_join_col
- self.select = [(select_alias, select_col)]
+ self.select = [SelectInfo((select_alias, select_col), None)]
self.remove_inherited_models()
def is_nullable(self, field):
@@ -2011,18 +1990,6 @@ def get_order_dir(field, default='ASC'):
return field, dirn[0]
-def setup_join_cache(sender, **kwargs):
- """
- The information needed to join between model fields is something that is
- invariant over the life of the model, so we cache it in the model's Options
- class, rather than recomputing it all the time.
-
- This method initialises the (empty) cache when the model is created.
- """
- sender._meta._join_cache = {}
-
-signals.class_prepared.connect(setup_join_cache)
-
def add_to_dict(data, key, value):
"""
A helper function to add "value" to the set of values for "key", whether or
@@ -2032,3 +1999,10 @@ def add_to_dict(data, key, value):
data[key].add(value)
else:
data[key] = set([value])
+
+def is_reverse_o2o(field):
+ """
+ A little helper to check if the given field is reverse-o2o. The field is
+ expected to be some sort of relation field or related object.
+ """
+ return not hasattr(field, 'rel') and field.field.unique
diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py
index 24ac957cbf..39d1ee0116 100644
--- a/django/db/models/sql/subqueries.py
+++ b/django/db/models/sql/subqueries.py
@@ -76,7 +76,7 @@ class DeleteQuery(Query):
return
else:
innerq.clear_select_clause()
- innerq.select, innerq.select_fields = [(self.get_initial_alias(), pk.column)], [None]
+ innerq.select = [SelectInfo((self.get_initial_alias(), pk.column), None)]
values = innerq
where = self.where_class()
where.add((Constraint(None, pk.column, pk), 'in', values), AND)
@@ -244,7 +244,7 @@ class DateQuery(Query):
alias = result[3][-1]
select = Date((alias, field.column), lookup_type)
self.clear_select_clause()
- self.select, self.select_fields = [select], [None]
+ self.select = [SelectInfo(select, None)]
self.distinct = True
self.order_by = order == 'ASC' and [1] or [-1]
diff --git a/django/db/utils.py b/django/db/utils.py
index 5fa78fe350..842fd354d6 100644
--- a/django/db/utils.py
+++ b/django/db/utils.py
@@ -5,6 +5,7 @@ from threading import local
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
+from django.utils._os import upath
from django.utils import six
@@ -27,7 +28,7 @@ def load_backend(backend_name):
except ImportError as e_user:
# The database backend wasn't found. Display a helpful error message
# listing all possible (built-in) database backends.
- backend_dir = os.path.join(os.path.dirname(__file__), 'backends')
+ backend_dir = os.path.join(os.path.dirname(upath(__file__)), 'backends')
try:
builtin_backends = [
name for _, name, ispkg in pkgutil.iter_modules([backend_dir])
@@ -53,7 +54,14 @@ class ConnectionDoesNotExist(Exception):
class ConnectionHandler(object):
def __init__(self, databases):
- self.databases = databases
+ if not databases:
+ self.databases = {
+ DEFAULT_DB_ALIAS: {
+ 'ENGINE': 'django.db.backends.dummy',
+ },
+ }
+ else:
+ self.databases = databases
self._connections = local()
def ensure_defaults(self, alias):
diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py
index 8d26e58bf4..65c5c408ff 100644
--- a/django/dispatch/dispatcher.py
+++ b/django/dispatch/dispatcher.py
@@ -10,6 +10,10 @@ def _make_id(target):
if hasattr(target, '__func__'):
return (id(target.__self__), id(target.__func__))
return id(target)
+NONE_ID = _make_id(None)
+
+# A marker for caching
+NO_RECEIVERS = object()
class Signal(object):
"""
@@ -20,8 +24,7 @@ class Signal(object):
receivers
{ receriverkey (id) : weakref(receiver) }
"""
-
- def __init__(self, providing_args=None):
+ def __init__(self, providing_args=None, use_caching=False):
"""
Create a new signal.
@@ -33,6 +36,13 @@ class Signal(object):
providing_args = []
self.providing_args = set(providing_args)
self.lock = threading.Lock()
+ self.use_caching = use_caching
+ # For convenience we create empty caches even if they are not used.
+ # A note about caching: if use_caching is defined, then for each
+ # distinct sender we cache the receivers that sender has in
+ # 'sender_receivers_cache'. The cache is cleaned when .connect() or
+ # .disconnect() is called and populated on send().
+ self.sender_receivers_cache = {}
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
"""
@@ -106,6 +116,7 @@ class Signal(object):
break
else:
self.receivers.append((lookup_key, receiver))
+ self.sender_receivers_cache = {}
def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None):
"""
@@ -140,9 +151,10 @@ class Signal(object):
if r_key == lookup_key:
del self.receivers[index]
break
+ self.sender_receivers_cache = {}
def has_listeners(self, sender=None):
- return bool(self._live_receivers(_make_id(sender)))
+ return bool(self._live_receivers(sender))
def send(self, sender, **named):
"""
@@ -163,10 +175,10 @@ class Signal(object):
Returns a list of tuple pairs [(receiver, response), ... ].
"""
responses = []
- if not self.receivers:
+ if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return responses
- for receiver in self._live_receivers(_make_id(sender)):
+ for receiver in self._live_receivers(sender):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
return responses
@@ -195,12 +207,12 @@ class Signal(object):
receiver.
"""
responses = []
- if not self.receivers:
+ if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return responses
# Call each receiver with whatever arguments it can accept.
# Return a list of tuple pairs [(receiver, response), ... ].
- for receiver in self._live_receivers(_make_id(sender)):
+ for receiver in self._live_receivers(sender):
try:
response = receiver(signal=self, sender=sender, **named)
except Exception as err:
@@ -209,26 +221,43 @@ class Signal(object):
responses.append((receiver, response))
return responses
- def _live_receivers(self, senderkey):
+ def _live_receivers(self, sender):
"""
Filter sequence of receivers to get resolved, live receivers.
This checks for weak references and resolves them, then returning only
live receivers.
"""
- none_senderkey = _make_id(None)
- receivers = []
-
- for (receiverkey, r_senderkey), receiver in self.receivers:
- if r_senderkey == none_senderkey or r_senderkey == senderkey:
- if isinstance(receiver, WEAKREF_TYPES):
- # Dereference the weak reference.
- receiver = receiver()
- if receiver is not None:
+ receivers = None
+ if self.use_caching:
+ receivers = self.sender_receivers_cache.get(sender)
+ # We could end up here with NO_RECEIVERS even if we do check this case in
+ # .send() prior to calling _live_receivers() due to concurrent .send() call.
+ if receivers is NO_RECEIVERS:
+ return []
+ if receivers is None:
+ with self.lock:
+ senderkey = _make_id(sender)
+ receivers = []
+ for (receiverkey, r_senderkey), receiver in self.receivers:
+ if r_senderkey == NONE_ID or r_senderkey == senderkey:
receivers.append(receiver)
- else:
- receivers.append(receiver)
- return receivers
+ if self.use_caching:
+ if not receivers:
+ self.sender_receivers_cache[sender] = NO_RECEIVERS
+ else:
+ # Note, we must cache the weakref versions.
+ self.sender_receivers_cache[sender] = receivers
+ non_weak_receivers = []
+ for receiver in receivers:
+ if isinstance(receiver, WEAKREF_TYPES):
+ # Dereference the weak reference.
+ receiver = receiver()
+ if receiver is not None:
+ non_weak_receivers.append(receiver)
+ else:
+ non_weak_receivers.append(receiver)
+ return non_weak_receivers
def _remove_receiver(self, receiver):
"""
@@ -246,8 +275,8 @@ class Signal(object):
# after we delete some items
for idx, (r_key, _) in enumerate(reversed(self.receivers)):
if r_key == key:
- del self.receivers[last_idx-idx]
-
+ del self.receivers[last_idx - idx]
+ self.sender_receivers_cache = {}
def receiver(signal, **kwargs):
"""
diff --git a/django/dispatch/saferef.py b/django/dispatch/saferef.py
index 84d1b2183c..7423669c96 100644
--- a/django/dispatch/saferef.py
+++ b/django/dispatch/saferef.py
@@ -67,9 +67,9 @@ class BoundMethodWeakref(object):
same BoundMethodWeakref instance.
"""
-
+
_allInstances = weakref.WeakValueDictionary()
-
+
def __new__( cls, target, onDelete=None, *arguments,**named ):
"""Create new instance or return current instance
@@ -92,7 +92,7 @@ class BoundMethodWeakref(object):
cls._allInstances[key] = base
base.__init__( target, onDelete, *arguments,**named)
return base
-
+
def __init__(self, target, onDelete=None):
"""Return a weak-reference-like instance for a bound method
@@ -132,7 +132,7 @@ class BoundMethodWeakref(object):
self.weakFunc = weakref.ref(target.__func__, remove)
self.selfName = str(target.__self__)
self.funcName = str(target.__func__.__name__)
-
+
def calculateKey( cls, target ):
"""Calculate the reference key for this reference
@@ -141,7 +141,7 @@ class BoundMethodWeakref(object):
"""
return (id(target.__self__),id(target.__func__))
calculateKey = classmethod( calculateKey )
-
+
def __str__(self):
"""Give a friendly representation of the object"""
return """%s( %s.%s )"""%(
@@ -157,14 +157,16 @@ class BoundMethodWeakref(object):
def __bool__( self ):
"""Whether we are still a valid reference"""
return self() is not None
- __nonzero__ = __bool__ # Python 2
+
+ def __nonzero__(self): # Python 2 compatibility
+ return type(self).__bool__(self)
def __eq__(self, other):
"""Compare with another reference"""
if not isinstance(other, self.__class__):
return self.__class__ == type(other)
return self.key == other.key
-
+
def __call__(self):
"""Return a strong reference to the bound method
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index c646eed506..3893cc54ba 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -69,7 +69,9 @@ class BaseFormSet(object):
def __bool__(self):
"""All formsets have a management form which is not included in the length"""
return True
- __nonzero__ = __bool__ # Python 2
+
+ def __nonzero__(self): # Python 2 compatibility
+ return type(self).__bool__(self)
@property
def management_form(self):
@@ -265,7 +267,7 @@ class BaseFormSet(object):
def is_valid(self):
"""
- Returns True if form.errors is empty for every form in self.forms.
+ Returns True if every form in self.forms is valid.
"""
if not self.is_bound:
return False
@@ -280,8 +282,7 @@ class BaseFormSet(object):
# This form is going to be deleted so any of its errors
# should not cause the entire formset to be invalid.
continue
- if bool(self.errors[i]):
- forms_valid = False
+ forms_valid &= form.is_valid()
return forms_valid and not bool(self.non_form_errors())
def full_clean(self):
diff --git a/django/forms/models.py b/django/forms/models.py
index 11fe0c09ea..e9b71ccf26 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -126,7 +126,7 @@ def model_to_dict(instance, fields=None, exclude=None):
data[f.name] = []
else:
# MultipleChoiceWidget needs a list of pks, not object instances.
- data[f.name] = [obj.pk for obj in f.value_from_object(instance)]
+ data[f.name] = list(f.value_from_object(instance).values_list('pk', flat=True))
else:
data[f.name] = f.value_from_object(instance)
return data
diff --git a/django/forms/widgets.py b/django/forms/widgets.py
index 763da0cff2..c761ea857d 100644
--- a/django/forms/widgets.py
+++ b/django/forms/widgets.py
@@ -403,7 +403,7 @@ class Textarea(Widget):
def render(self, name, value, attrs=None):
if value is None: value = ''
final_attrs = self.build_attrs(attrs, name=name)
- return format_html('<textarea{0}>{1}</textarea>',
+ return format_html('<textarea{0}>\r\n{1}</textarea>',
flatatt(final_attrs),
force_text(value))
@@ -528,7 +528,7 @@ class CheckboxInput(Widget):
values = {'true': True, 'false': False}
if isinstance(value, six.string_types):
value = values.get(value.lower(), value)
- return value
+ return bool(value)
def _has_changed(self, initial, data):
# Sometimes data or initial could be None or '' which should be the
diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py
index 40aefd6e9d..edf98f6e49 100644
--- a/django/http/multipartparser.py
+++ b/django/http/multipartparser.py
@@ -110,7 +110,7 @@ class MultiPartParser(object):
# HTTP spec says that Content-Length >= 0 is valid
# handling content-length == 0 before continuing
if self._content_length == 0:
- return QueryDict(MultiValueDict(), encoding=self._encoding), MultiValueDict()
+ return QueryDict('', encoding=self._encoding), MultiValueDict()
# See if the handler will want to take care of the parsing.
# This allows overriding everything if somebody wants it.
@@ -199,6 +199,12 @@ class MultiPartParser(object):
for chunk in field_stream:
if transfer_encoding == 'base64':
# We only special-case base64 transfer encoding
+ # We should always read base64 streams by multiple of 4
+ over_bytes = len(chunk) % 4
+ if over_bytes:
+ over_chunk = field_stream.read(4 - over_bytes)
+ chunk += over_chunk
+
try:
chunk = base64.b64decode(chunk)
except Exception as e:
@@ -256,7 +262,7 @@ class MultiPartParser(object):
"""Cleanup filename from Internet Explorer full paths."""
return filename and filename[filename.rfind("\\")+1:].strip()
-class LazyStream(object):
+class LazyStream(six.Iterator):
"""
The LazyStream wrapper allows one to get and "unget" bytes from a stream.
@@ -323,8 +329,6 @@ class LazyStream(object):
self.position += len(output)
return output
- next = __next__ # Python 2 compatibility
-
def close(self):
"""
Used to invalidate/disable this lazy stream.
@@ -369,7 +373,7 @@ class LazyStream(object):
" if there is none, report this to the Django developers."
)
-class ChunkIter(object):
+class ChunkIter(six.Iterator):
"""
An iterable that will yield chunks of data. Given a file-like object as the
constructor, this object will yield chunks of read operations from that
@@ -389,12 +393,10 @@ class ChunkIter(object):
else:
raise StopIteration()
- next = __next__ # Python 2 compatibility
-
def __iter__(self):
return self
-class InterBoundaryIter(object):
+class InterBoundaryIter(six.Iterator):
"""
A Producer that will iterate over boundaries.
"""
@@ -411,9 +413,7 @@ class InterBoundaryIter(object):
except InputStreamExhausted:
raise StopIteration()
- next = __next__ # Python 2 compatibility
-
-class BoundaryIter(object):
+class BoundaryIter(six.Iterator):
"""
A Producer that is sensitive to boundaries.
@@ -489,8 +489,6 @@ class BoundaryIter(object):
stream.unget(chunk[-rollback:])
return chunk[:-rollback]
- next = __next__ # Python 2 compatibility
-
def _find_boundary(self, data, eof = False):
"""
Finds a multipart boundary in data.
diff --git a/django/http/request.py b/django/http/request.py
index 96c7606c86..8f74bddb71 100644
--- a/django/http/request.py
+++ b/django/http/request.py
@@ -25,6 +25,7 @@ from django.utils.encoding import force_bytes, force_text, force_str, iri_to_uri
RAISE_ERROR = object()
absolute_http_url_re = re.compile(r"^https?://", re.I)
+host_validation_re = re.compile(r"^([a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9:]+\])(:\d+)?$")
class UnreadablePostError(IOError):
@@ -64,7 +65,7 @@ class HttpRequest(object):
host = '%s:%s' % (host, server_port)
# Disallow potentially poisoned hostnames.
- if set(';/?@&=+$,').intersection(host):
+ if not host_validation_re.match(host.lower()):
raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host)
return host
@@ -276,6 +277,9 @@ class QueryDict(MultiValueDict):
encoding = settings.DEFAULT_CHARSET
self.encoding = encoding
if six.PY3:
+ if isinstance(query_string, bytes):
+ # query_string contains URL-encoded data, a subset of ASCII.
+ query_string = query_string.decode()
for key, value in parse_qsl(query_string or '',
keep_blank_values=True,
encoding=encoding):
diff --git a/django/http/response.py b/django/http/response.py
index 56e3d00096..df0a955b18 100644
--- a/django/http/response.py
+++ b/django/http/response.py
@@ -23,7 +23,7 @@ class BadHeaderError(ValueError):
pass
-class HttpResponseBase(object):
+class HttpResponseBase(six.Iterator):
"""
An HTTP response base class with dictionary-accessed headers.
@@ -218,8 +218,6 @@ class HttpResponseBase(object):
# Subclasses must define self._iterator for this function.
return self.make_bytes(next(self._iterator))
- next = __next__ # Python 2 compatibility
-
# These methods partially implement the file-like object interface.
# See http://docs.python.org/lib/bltin-file-objects.html
diff --git a/django/middleware/cache.py b/django/middleware/cache.py
index 34bf0ca4a4..de2b86f630 100644
--- a/django/middleware/cache.py
+++ b/django/middleware/cache.py
@@ -126,7 +126,6 @@ class FetchFromCacheMiddleware(object):
def __init__(self):
self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX
- self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False)
self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS
self.cache = get_cache(self.cache_alias)
diff --git a/django/middleware/common.py b/django/middleware/common.py
index 6fbbf43044..ccc9fbfaad 100644
--- a/django/middleware/common.py
+++ b/django/middleware/common.py
@@ -6,6 +6,7 @@ from django.conf import settings
from django import http
from django.core.mail import mail_managers
from django.utils.http import urlquote
+from django.utils import six
from django.core import urlresolvers
@@ -87,7 +88,17 @@ class CommonMiddleware(object):
else:
newurl = urlquote(new_url[1])
if request.META.get('QUERY_STRING', ''):
- newurl += '?' + request.META['QUERY_STRING']
+ if six.PY3:
+ newurl += '?' + request.META['QUERY_STRING']
+ else:
+ # `query_string` is a bytestring. Appending it to the unicode
+ # string `newurl` will fail if it isn't ASCII-only. This isn't
+ # allowed; only broken software generates such query strings.
+ # Better drop the invalid query string than crash (#15152).
+ try:
+ newurl += '?' + request.META['QUERY_STRING'].decode()
+ except UnicodeDecodeError:
+ pass
return http.HttpResponsePermanentRedirect(newurl)
def process_response(self, request, response):
diff --git a/django/shortcuts/__init__.py b/django/shortcuts/__init__.py
index a824446b7e..9f896347a4 100644
--- a/django/shortcuts/__init__.py
+++ b/django/shortcuts/__init__.py
@@ -7,6 +7,7 @@ for convenience's sake.
from django.template import loader, RequestContext
from django.http import HttpResponse, Http404
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
+from django.db.models.base import ModelBase
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
from django.core import urlresolvers
@@ -46,7 +47,7 @@ def render(request, *args, **kwargs):
def redirect(to, *args, **kwargs):
"""
- Returns an HttpResponseRedirect to the apropriate URL for the arguments
+ Returns an HttpResponseRedirect to the appropriate URL for the arguments
passed.
The arguments could be:
@@ -72,13 +73,20 @@ def _get_queryset(klass):
"""
Returns a QuerySet from a Model, Manager, or QuerySet. Created to make
get_object_or_404 and get_list_or_404 more DRY.
+
+ Raises a ValueError if klass is not a Model, Manager, or QuerySet.
"""
if isinstance(klass, QuerySet):
return klass
elif isinstance(klass, Manager):
manager = klass
- else:
+ elif isinstance(klass, ModelBase):
manager = klass._default_manager
+ else:
+ klass__name = klass.__name__ if isinstance(klass, type) \
+ else klass.__class__.__name__
+ raise ValueError("Object is of type '%s', but must be a Django Model, "
+ "Manager, or QuerySet" % klass__name)
return manager.all()
def get_object_or_404(klass, *args, **kwargs):
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index e15440f90e..dac4e5ddb4 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -704,7 +704,7 @@ def get_digit(value, arg):
@register.filter(expects_localtime=True, is_safe=False)
def date(value, arg=None):
"""Formats a date according to the given format."""
- if not value:
+ if value in (None, ''):
return ''
if arg is None:
arg = settings.DATE_FORMAT
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index fa2e840cbf..aca2f41f2d 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -398,10 +398,14 @@ class URLNode(Node):
view_name = self.view_name.resolve(context)
+ if not view_name:
+ raise NoReverseMatch("'url' requires a non-empty first argument. "
+ "The syntax changed in Django 1.5, see the docs.")
+
# Try to look up the URL twice: once given the view name, and again
# relative to what we guess is the "main" app. If they both fail,
# re-raise the NoReverseMatch unless we're using the
- # {% url ... as var %} construct in which cause return nothing.
+ # {% url ... as var %} construct in which case return nothing.
url = ''
try:
url = reverse(view_name, args=args, kwargs=kwargs, current_app=context.current_app)
@@ -1258,7 +1262,12 @@ def url(parser, token):
if len(bits) < 2:
raise TemplateSyntaxError("'%s' takes at least one argument"
" (path to a view)" % bits[0])
- viewname = parser.compile_filter(bits[1])
+ try:
+ viewname = parser.compile_filter(bits[1])
+ except TemplateSyntaxError as exc:
+ exc.args = (exc.args[0] + ". "
+ "The syntax of 'url' changed in Django 1.5, see the docs."),
+ raise
args = []
kwargs = {}
asvar = None
diff --git a/django/template/response.py b/django/template/response.py
index 2cb44d127d..3b3b41331a 100644
--- a/django/template/response.py
+++ b/django/template/response.py
@@ -40,7 +40,7 @@ class SimpleTemplateResponse(HttpResponse):
rendered, and that the pickled state only includes rendered
data, not the data used to construct the response.
"""
- obj_dict = self.__dict__.copy()
+ obj_dict = super(SimpleTemplateResponse, self).__getstate__()
if not self._is_rendered:
raise ContentNotRenderedError('The response content must be '
'rendered before it can be pickled.')
diff --git a/django/test/signals.py b/django/test/signals.py
index d140304f1d..a96bdff3b3 100644
--- a/django/test/signals.py
+++ b/django/test/signals.py
@@ -5,6 +5,7 @@ from django.conf import settings
from django.db import connections
from django.dispatch import receiver, Signal
from django.utils import timezone
+from django.utils.functional import empty
template_rendered = Signal(providing_args=["template", "context"])
@@ -72,3 +73,9 @@ def language_changed(**kwargs):
trans_real._default = None
if kwargs['setting'] == 'LOCALE_PATHS':
trans_real._translations = {}
+
+@receiver(setting_changed)
+def file_storage_changed(**kwargs):
+ if kwargs['setting'] in ('MEDIA_ROOT', 'DEFAULT_FILE_STORAGE'):
+ from django.core.files.storage import default_storage
+ default_storage._wrapped = empty
diff --git a/django/test/testcases.py b/django/test/testcases.py
index 1239275264..3af6b8c346 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -241,6 +241,40 @@ class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext):
class SimpleTestCase(ut2.TestCase):
+ def __call__(self, result=None):
+ """
+ Wrapper around default __call__ method to perform common Django test
+ set up. This means that user-defined Test Cases aren't required to
+ include a call to super().setUp().
+ """
+ testMethod = getattr(self, self._testMethodName)
+ skipped = (getattr(self.__class__, "__unittest_skip__", False) or
+ getattr(testMethod, "__unittest_skip__", False))
+
+ if not skipped:
+ try:
+ self._pre_setup()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except Exception:
+ result.addError(self, sys.exc_info())
+ return
+ super(SimpleTestCase, self).__call__(result)
+ if not skipped:
+ try:
+ self._post_teardown()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except Exception:
+ result.addError(self, sys.exc_info())
+ return
+
+ def _pre_setup(self):
+ pass
+
+ def _post_teardown(self):
+ pass
+
def save_warnings_state(self):
"""
Saves the state of the warnings module
@@ -412,10 +446,20 @@ class TransactionTestCase(SimpleTestCase):
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:
@@ -433,10 +477,7 @@ class TransactionTestCase(SimpleTestCase):
transaction.commit_unless_managed(using=db_name)
def _fixture_setup(self):
- # If the test case has a multi_db=True flag, act on all databases.
- # Otherwise, just on the default DB.
- db_names = connections if getattr(self, 'multi_db', False) else [DEFAULT_DB_ALIAS]
- for db_name in db_names:
+ for db_name in self._databases_names(include_mirrors=False):
# Reset sequences
if self.reset_sequences:
self._reset_sequences(db_name)
@@ -453,35 +494,6 @@ class TransactionTestCase(SimpleTestCase):
settings.ROOT_URLCONF = self.urls
clear_url_caches()
- def __call__(self, result=None):
- """
- Wrapper around default __call__ method to perform common Django test
- set up. This means that user-defined Test Cases aren't required to
- include a call to super().setUp().
- """
- testMethod = getattr(self, self._testMethodName)
- skipped = (getattr(self.__class__, "__unittest_skip__", False) or
- getattr(testMethod, "__unittest_skip__", False))
-
- if not skipped:
- self.client = self.client_class()
- try:
- self._pre_setup()
- except (KeyboardInterrupt, SystemExit):
- raise
- except Exception:
- result.addError(self, sys.exc_info())
- return
- super(TransactionTestCase, self).__call__(result)
- if not skipped:
- try:
- self._post_teardown()
- except (KeyboardInterrupt, SystemExit):
- raise
- except Exception:
- result.addError(self, sys.exc_info())
- return
-
def _post_teardown(self):
""" Performs any post-test things. This includes:
@@ -502,10 +514,12 @@ class TransactionTestCase(SimpleTestCase):
conn.close()
def _fixture_teardown(self):
- # If the test case has a multi_db=True flag, flush all databases.
- # Otherwise, just flush default.
- databases = connections if getattr(self, 'multi_db', False) else [DEFAULT_DB_ALIAS]
- for db in databases:
+ # Roll back any pending transactions in order to avoid a deadlock
+ # during flush when TEST_MIRROR is used (#18984).
+ for conn in connections.all():
+ conn.rollback_unless_managed()
+
+ for db in self._databases_names(include_mirrors=False):
call_command('flush', verbosity=0, interactive=False, database=db,
skip_validation=True, reset_sequences=False)
@@ -753,6 +767,12 @@ class TransactionTestCase(SimpleTestCase):
items = six.moves.map(transform, qs)
if not ordered:
return self.assertEqual(set(items), set(values))
+ values = list(values)
+ # For example qs.iterator() could be passed as qs, but it does not
+ # have 'ordered' attribute.
+ if len(values) > 1 and hasattr(qs, 'ordered') and not qs.ordered:
+ raise ValueError("Trying to compare non-ordered queryset "
+ "against more than one ordered values")
return self.assertEqual(list(items), values)
def assertNumQueries(self, num, func=None, *args, **kwargs):
@@ -790,11 +810,7 @@ class TestCase(TransactionTestCase):
assert not self.reset_sequences, 'reset_sequences cannot be used on TestCase instances'
- # If the test case has a multi_db=True flag, setup all databases.
- # Otherwise, just use default.
- db_names = connections if getattr(self, 'multi_db', False) else [DEFAULT_DB_ALIAS]
-
- for db_name in db_names:
+ for db_name in self._databases_names():
transaction.enter_transaction_management(using=db_name)
transaction.managed(True, using=db_name)
disable_transaction_methods()
@@ -802,7 +818,7 @@ class TestCase(TransactionTestCase):
from django.contrib.sites.models import Site
Site.objects.clear_cache()
- for db in db_names:
+ for db in self._databases_names(include_mirrors=False):
if hasattr(self, 'fixtures'):
call_command('loaddata', *self.fixtures,
**{
@@ -816,15 +832,8 @@ class TestCase(TransactionTestCase):
if not connections_support_transactions():
return super(TestCase, self)._fixture_teardown()
- # If the test case has a multi_db=True flag, teardown all databases.
- # Otherwise, just teardown default.
- if getattr(self, 'multi_db', False):
- databases = connections
- else:
- databases = [DEFAULT_DB_ALIAS]
-
restore_transaction_methods()
- for db in databases:
+ for db in self._databases_names():
transaction.rollback(using=db)
transaction.leave_transaction_management(using=db)
@@ -1025,6 +1034,7 @@ class LiveServerThread(threading.Thread):
(self.host, port), QuietWSGIRequestHandler)
except WSGIServerException as e:
if (index + 1 < len(self.possible_ports) and
+ hasattr(e.args[0], 'errno') and
e.args[0].errno == errno.EADDRINUSE):
# This port is already in use, so we go on and try with
# the next one in the list.
@@ -1077,7 +1087,7 @@ class LiveServerTestCase(TransactionTestCase):
for conn in connections.all():
# If using in-memory sqlite databases, pass the connections to
# the server thread.
- if (conn.settings_dict['ENGINE'] == 'django.db.backends.sqlite3'
+ if (conn.settings_dict['ENGINE'].rsplit('.', 1)[-1] in ('sqlite3', 'spatialite')
and conn.settings_dict['NAME'] == ':memory:'):
# Explicitly enable thread-shareability for this connection
conn.allow_thread_sharing = True
@@ -1129,7 +1139,7 @@ class LiveServerTestCase(TransactionTestCase):
# Restore sqlite connections' non-sharability
for conn in connections.all():
- if (conn.settings_dict['ENGINE'] == 'django.db.backends.sqlite3'
+ if (conn.settings_dict['ENGINE'].rsplit('.', 1)[-1] in ('sqlite3', 'spatialite')
and conn.settings_dict['NAME'] == ':memory:'):
conn.allow_thread_sharing = False
diff --git a/django/test/utils.py b/django/test/utils.py
index f10d388227..8114ae0e6a 100644
--- a/django/test/utils.py
+++ b/django/test/utils.py
@@ -187,8 +187,12 @@ class override_settings(object):
self.disable()
def __call__(self, test_func):
- from django.test import TransactionTestCase
- if isinstance(test_func, type) and issubclass(test_func, TransactionTestCase):
+ from django.test import SimpleTestCase
+ if isinstance(test_func, type):
+ if not issubclass(test_func, SimpleTestCase):
+ raise Exception(
+ "Only subclasses of Django SimpleTestCase can be decorated "
+ "with override_settings")
original_pre_setup = test_func._pre_setup
original_post_teardown = test_func._post_teardown
diff --git a/django/utils/_os.py b/django/utils/_os.py
index 1ea12aed8a..6c1cd17a83 100644
--- a/django/utils/_os.py
+++ b/django/utils/_os.py
@@ -1,6 +1,8 @@
import os
import stat
+import sys
from os.path import join, normcase, normpath, abspath, isabs, sep, dirname
+
from django.utils.encoding import force_text
from django.utils import six
@@ -10,6 +12,9 @@ except NameError:
class WindowsError(Exception):
pass
+if not six.PY3:
+ fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
+
# Under Python 2, define our own abspath function that can handle joining
# unicode paths to a current working directory that has non-ASCII characters
@@ -29,6 +34,23 @@ else:
path = join(os.getcwdu(), path)
return normpath(path)
+def upath(path):
+ """
+ Always return a unicode path.
+ """
+ if not six.PY3:
+ return path.decode(fs_encoding)
+ return path
+
+def npath(path):
+ """
+ Always return a native path, that is unicode on Python 3 and bytestring on
+ Python 2.
+ """
+ if not six.PY3 and not isinstance(path, bytes):
+ return path.encode(fs_encoding)
+ return path
+
def safe_join(base, *paths):
"""
Joins one or more path components to the base path component intelligently.
@@ -74,4 +96,3 @@ def rmtree_errorhandler(func, path, exc_info):
os.chmod(path, stat.S_IWRITE)
# use the original function to repeat the operation
func(path)
-
diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py
index 2daafedd85..617fc9da7d 100644
--- a/django/utils/autoreload.py
+++ b/django/utils/autoreload.py
@@ -28,7 +28,7 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-import os, sys, time, signal
+import os, sys, time, signal, traceback
try:
from django.utils.six.moves import _thread as thread
@@ -52,10 +52,17 @@ RUN_RELOADER = True
_mtimes = {}
_win = (sys.platform == "win32")
+_error_files = []
+
def code_changed():
global _mtimes, _win
- filenames = [getattr(m, "__file__", None) for m in sys.modules.values()]
- for filename in filter(None, filenames):
+ filenames = []
+ for m in sys.modules.values():
+ try:
+ filenames.append(m.__file__)
+ except AttributeError:
+ pass
+ for filename in filenames + _error_files:
if filename.endswith(".pyc") or filename.endswith(".pyo"):
filename = filename[:-1]
if filename.endswith("$py.class"):
@@ -71,9 +78,34 @@ def code_changed():
continue
if mtime != _mtimes[filename]:
_mtimes = {}
+ try:
+ del _error_files[_error_files.index(filename)]
+ except ValueError:
+ pass
return True
return False
+def check_errors(fn):
+ def wrapper(*args, **kwargs):
+ try:
+ fn(*args, **kwargs)
+ except (ImportError, IndentationError, NameError, SyntaxError,
+ TypeError, AttributeError):
+ et, ev, tb = sys.exc_info()
+
+ if getattr(ev, 'filename', None) is None:
+ # get the filename from the last item in the stack
+ filename = traceback.extract_tb(tb)[-1][0]
+ else:
+ filename = ev.filename
+
+ if filename not in _error_files:
+ _error_files.append(filename)
+
+ raise
+
+ return wrapper
+
def ensure_echo_on():
if termios:
fd = sys.stdin
@@ -142,5 +174,7 @@ def main(main_func, args=None, kwargs=None):
reloader = jython_reloader
else:
reloader = python_reloader
- reloader(main_func, args, kwargs)
+
+ wrapped_main_func = check_errors(main_func)
+ reloader(wrapped_main_func, args, kwargs)
diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py
index d94a05dfb4..f81fb88a19 100644
--- a/django/utils/datastructures.py
+++ b/django/utils/datastructures.py
@@ -1,6 +1,5 @@
import copy
import warnings
-from types import GeneratorType
from django.utils import six
@@ -120,27 +119,23 @@ class SortedDict(dict):
return instance
def __init__(self, data=None):
- if data is None:
- data = {}
- elif isinstance(data, GeneratorType):
- # Unfortunately we need to be able to read a generator twice. Once
- # to get the data into self with our super().__init__ call and a
- # second time to setup keyOrder correctly
- data = list(data)
- super(SortedDict, self).__init__(data)
- if isinstance(data, dict):
- self.keyOrder = list(data)
+ if data is None or isinstance(data, dict):
+ data = data or []
+ super(SortedDict, self).__init__(data)
+ self.keyOrder = list(data) if data else []
else:
- self.keyOrder = []
- seen = set()
+ super(SortedDict, self).__init__()
+ super_set = super(SortedDict, self).__setitem__
for key, value in data:
- if key not in seen:
+ # Take the ordering from first key
+ if key not in self:
self.keyOrder.append(key)
- seen.add(key)
+ # But override with last value in data (dict() does this)
+ super_set(key, value)
def __deepcopy__(self, memo):
return self.__class__([(key, copy.deepcopy(value, memo))
- for key, value in six.iteritems(self)])
+ for key, value in self.items()])
def __copy__(self):
# The Python's default copy implementation will alter the state
@@ -199,13 +194,13 @@ class SortedDict(dict):
itervalues = _itervalues
def items(self):
- return list(self.iteritems())
+ return [(k, self[k]) for k in self.keyOrder]
def keys(self):
- return list(self.iterkeys())
+ return self.keyOrder[:]
def values(self):
- return list(self.itervalues())
+ return [self[k] for k in self.keyOrder]
def update(self, dict_):
for k, v in six.iteritems(dict_):
diff --git a/django/utils/encoding.py b/django/utils/encoding.py
index b0872471c2..15215849b2 100644
--- a/django/utils/encoding.py
+++ b/django/utils/encoding.py
@@ -216,7 +216,7 @@ def iri_to_uri(iri):
return quote(force_bytes(iri), safe=b"/#%[]=:;$&()+,!?*@'~")
def filepath_to_uri(path):
- """Convert an file system path to a URI portion that is suitable for
+ """Convert a file system path to a URI portion that is suitable for
inclusion in a URL.
We are assuming input is either UTF-8 or unicode already.
diff --git a/django/utils/formats.py b/django/utils/formats.py
index 555982eede..03b9918edf 100644
--- a/django/utils/formats.py
+++ b/django/utils/formats.py
@@ -16,6 +16,17 @@ from django.utils.translation import get_language, to_locale, check_for_language
_format_cache = {}
_format_modules_cache = {}
+ISO_INPUT_FORMATS = {
+ 'DATE_INPUT_FORMATS': ('%Y-%m-%d',),
+ 'TIME_INPUT_FORMATS': ('%H:%M:%S', '%H:%M'),
+ 'DATETIME_INPUT_FORMATS': (
+ '%Y-%m-%d %H:%M:%S',
+ '%Y-%m-%d %H:%M:%S.%f',
+ '%Y-%m-%d %H:%M',
+ '%Y-%m-%d'
+ ),
+}
+
def reset_format_cache():
"""Clear any cached formats.
@@ -82,6 +93,11 @@ def get_format(format_type, lang=None, use_l10n=None):
for module in get_format_modules(lang):
try:
val = getattr(module, format_type)
+ for iso_input in ISO_INPUT_FORMATS.get(format_type, ()):
+ if iso_input not in val:
+ if isinstance(val, tuple):
+ val = list(val)
+ val.append(iso_input)
_format_cache[cache_key] = val
return val
except AttributeError:
diff --git a/django/utils/functional.py b/django/utils/functional.py
index 505931e158..661518e3cc 100644
--- a/django/utils/functional.py
+++ b/django/utils/functional.py
@@ -5,9 +5,9 @@ import sys
from django.utils import six
-# You can't trivially replace this `functools.partial` because this binds to
-# classes and returns bound instances, whereas functools.partial (on CPython)
-# is a type and its instances don't bind.
+# You can't trivially replace this with `functools.partial` because this binds
+# to classes and returns bound instances, whereas functools.partial (on
+# CPython) is a type and its instances don't bind.
def curry(_curried_func, *args, **kwargs):
def _curried(*moreargs, **morekwargs):
return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs))
@@ -33,8 +33,8 @@ def memoize(func, cache, num_args):
class cached_property(object):
"""
- Decorator that creates converts a method with a single
- self argument into a property cached on the instance.
+ Decorator that converts a method with a single self argument into a
+ property cached on the instance.
"""
def __init__(self, func):
self.func = func
diff --git a/django/utils/html.py b/django/utils/html.py
index cc8372906b..25605bea04 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -18,7 +18,7 @@ from django.utils.text import normalize_newlines
# Configuration for urlize() function.
TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)']
-WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('&lt;', '&gt;')]
+WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('&lt;', '&gt;')]
# List of possible strings used for bullets in bulleted lists.
DOTS = ['&middot;', '*', '\u2022', '&#149;', '&bull;', '&#8226;']
@@ -33,6 +33,7 @@ link_target_attribute_re = re.compile(r'(<a [^>]*?)target=[^\s>]+')
html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE)
hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL)
trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
+strip_tags_re = re.compile(r'</?\S([^=]*=(\s*"[^"]*"|\s*\'[^\']*\'|\S*)|[^>])*?>', re.IGNORECASE)
def escape(text):
@@ -117,7 +118,7 @@ linebreaks = allow_lazy(linebreaks, six.text_type)
def strip_tags(value):
"""Returns the given HTML with all tags stripped."""
- return re.sub(r'<[^>]*?>', '', force_text(value))
+ return strip_tags_re.sub('', force_text(value))
strip_tags = allow_lazy(strip_tags)
def remove_tags(html, tags):
@@ -149,13 +150,17 @@ fix_ampersands = allow_lazy(fix_ampersands, six.text_type)
def smart_urlquote(url):
"Quotes a URL if it isn't already quoted."
# Handle IDN before quoting.
- scheme, netloc, path, query, fragment = urlsplit(url)
try:
- netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
- except UnicodeError: # invalid domain part
+ scheme, netloc, path, query, fragment = urlsplit(url)
+ try:
+ netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
+ except UnicodeError: # invalid domain part
+ pass
+ else:
+ url = urlunsplit((scheme, netloc, path, query, fragment))
+ except ValueError:
+ # invalid IPv6 URL (normally square brackets in hostname part).
pass
- else:
- url = urlunsplit((scheme, netloc, path, query, fragment))
# An URL is considered unquoted if it contains no % characters or
# contains a % not followed by two hexadecimal digits. See #9655.
diff --git a/django/utils/http.py b/django/utils/http.py
index 1c3b0039b5..0ab5198804 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -227,3 +227,15 @@ def same_origin(url1, url2):
"""
p1, p2 = urllib_parse.urlparse(url1), urllib_parse.urlparse(url2)
return (p1.scheme, p1.hostname, p1.port) == (p2.scheme, p2.hostname, p2.port)
+
+def is_safe_url(url, host=None):
+ """
+ Return ``True`` if the url is a safe redirection (i.e. it doesn't point to
+ a different host).
+
+ Always returns ``False`` on an empty url.
+ """
+ if not url:
+ return False
+ netloc = urllib_parse.urlparse(url)[1]
+ return not netloc or netloc == host
diff --git a/django/utils/log.py b/django/utils/log.py
index ea0122794b..736154a178 100644
--- a/django/utils/log.py
+++ b/django/utils/log.py
@@ -3,6 +3,7 @@ import traceback
from django.conf import settings
from django.core import mail
+from django.core.mail import get_connection
from django.views.debug import ExceptionReporter, get_exception_reporter_filter
@@ -39,7 +40,7 @@ DEFAULT_LOGGING = {
},
},
'handlers': {
- 'console':{
+ 'console': {
'level': 'INFO',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
@@ -62,6 +63,9 @@ DEFAULT_LOGGING = {
'level': 'ERROR',
'propagate': False,
},
+ 'py.warnings': {
+ 'handlers': ['console'],
+ },
}
}
@@ -73,9 +77,10 @@ class AdminEmailHandler(logging.Handler):
request data will be provided in the email report.
"""
- def __init__(self, include_html=False):
+ def __init__(self, include_html=False, email_backend=None):
logging.Handler.__init__(self)
self.include_html = include_html
+ self.email_backend = email_backend
def emit(self, record):
try:
@@ -107,7 +112,12 @@ class AdminEmailHandler(logging.Handler):
message = "%s\n\n%s" % (stack_trace, request_repr)
reporter = ExceptionReporter(request, is_email=True, *exc_info)
html_message = self.include_html and reporter.get_traceback_html() or None
- mail.mail_admins(subject, message, fail_silently=True, html_message=html_message)
+ mail.mail_admins(subject, message, fail_silently=True,
+ html_message=html_message,
+ connection=self.connection())
+
+ def connection(self):
+ return get_connection(backend=self.email_backend)
def format_subject(self, subject):
"""
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index 9e94840ee0..cf6270cc0c 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -10,6 +10,7 @@ from threading import local
from django.utils.importlib import import_module
from django.utils.encoding import force_str, force_text
+from django.utils._os import upath
from django.utils.safestring import mark_safe, SafeData
from django.utils import six
from django.utils.six import StringIO
@@ -109,7 +110,7 @@ def translation(language):
from django.conf import settings
- globalpath = os.path.join(os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
+ globalpath = os.path.join(os.path.dirname(upath(sys.modules[settings.__module__].__file__)), 'locale')
def _fetch(lang, fallback=None):
@@ -151,7 +152,7 @@ def translation(language):
for appname in reversed(settings.INSTALLED_APPS):
app = import_module(appname)
- apppath = os.path.join(os.path.dirname(app.__file__), 'locale')
+ apppath = os.path.join(os.path.dirname(upath(app.__file__)), 'locale')
if os.path.isdir(apppath):
res = _merge(apppath)
@@ -246,7 +247,8 @@ def do_translate(message, translation_function):
"""
global _default
- eol_message = message.replace('\r\n', '\n').replace('\r', '\n')
+ # str() is allowing a bytestring message to remain bytestring on Python 2
+ eol_message = message.replace(str('\r\n'), str('\n')).replace(str('\r'), str('\n'))
t = getattr(_active, "value", None)
if t is not None:
result = getattr(t, translation_function)(eol_message)
@@ -336,7 +338,7 @@ def all_locale_paths():
"""
from django.conf import settings
globalpath = os.path.join(
- os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
+ os.path.dirname(upath(sys.modules[settings.__module__].__file__)), 'locale')
return [globalpath] + list(settings.LOCALE_PATHS)
def check_for_language(lang_code):
diff --git a/django/utils/tree.py b/django/utils/tree.py
index 717181d2b9..ce490224e0 100644
--- a/django/utils/tree.py
+++ b/django/utils/tree.py
@@ -73,7 +73,9 @@ class Node(object):
For truth value testing.
"""
return bool(self.children)
- __nonzero__ = __bool__ # Python 2
+
+ def __nonzero__(self): # Python 2 compatibility
+ return type(self).__bool__(self)
def __contains__(self, other):
"""
diff --git a/django/views/generic/base.py b/django/views/generic/base.py
index 23e18c54a0..9c82a29d8a 100644
--- a/django/views/generic/base.py
+++ b/django/views/generic/base.py
@@ -54,13 +54,17 @@ class View(object):
"keyword argument to %s(). Don't do that."
% (key, cls.__name__))
if not hasattr(cls, key):
- raise TypeError("%s() received an invalid keyword %r" % (
- cls.__name__, key))
+ raise TypeError("%s() received an invalid keyword %r. as_view "
+ "only accepts arguments that are already "
+ "attributes of the class." % (cls.__name__, key))
def view(request, *args, **kwargs):
self = cls(**initkwargs)
if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get
+ self.request = request
+ self.args = args
+ self.kwargs = kwargs
return self.dispatch(request, *args, **kwargs)
# take name and docstring from class
@@ -79,9 +83,6 @@ class View(object):
handler = getattr(self, request.method.lower(), self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
- self.request = request
- self.args = args
- self.kwargs = kwargs
return handler(request, *args, **kwargs)
def http_method_not_allowed(self, request, *args, **kwargs):
diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py
index e51cdf5a3d..97a6c0a698 100644
--- a/django/views/generic/edit.py
+++ b/django/views/generic/edit.py
@@ -1,6 +1,7 @@
from django.forms import models as model_forms
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
+from django.utils.encoding import force_text
from django.views.generic.base import TemplateResponseMixin, ContextMixin, View
from django.views.generic.detail import (SingleObjectMixin,
SingleObjectTemplateResponseMixin, BaseDetailView)
@@ -50,7 +51,8 @@ class FormMixin(ContextMixin):
Returns the supplied success URL.
"""
if self.success_url:
- url = self.success_url
+ # Forcing possible reverse_lazy evaluation
+ url = force_text(self.success_url)
else:
raise ImproperlyConfigured(
"No URL to redirect to. Provide a success_url.")
diff --git a/django/views/generic/list.py b/django/views/generic/list.py
index ec30c58f29..1f286168f6 100644
--- a/django/views/generic/list.py
+++ b/django/views/generic/list.py
@@ -15,8 +15,10 @@ class MultipleObjectMixin(ContextMixin):
queryset = None
model = None
paginate_by = None
+ paginate_orphans = 0
context_object_name = None
paginator_class = Paginator
+ page_kwarg = 'page'
def get_queryset(self):
"""
@@ -38,8 +40,11 @@ class MultipleObjectMixin(ContextMixin):
"""
Paginate the queryset, if needed.
"""
- paginator = self.get_paginator(queryset, page_size, allow_empty_first_page=self.get_allow_empty())
- page = self.kwargs.get('page') or self.request.GET.get('page') or 1
+ paginator = self.get_paginator(
+ queryset, page_size, orphans=self.get_paginate_orphans(),
+ allow_empty_first_page=self.get_allow_empty())
+ page_kwarg = self.page_kwarg
+ page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
try:
page_number = int(page)
except ValueError:
@@ -50,9 +55,10 @@ class MultipleObjectMixin(ContextMixin):
try:
page = paginator.page(page_number)
return (paginator, page, page.object_list, page.has_other_pages())
- except InvalidPage:
- raise Http404(_('Invalid page (%(page_number)s)') % {
- 'page_number': page_number
+ except InvalidPage as e:
+ raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
+ 'page_number': page_number,
+ 'message': str(e)
})
def get_paginate_by(self, queryset):
@@ -61,11 +67,21 @@ class MultipleObjectMixin(ContextMixin):
"""
return self.paginate_by
- def get_paginator(self, queryset, per_page, orphans=0, allow_empty_first_page=True):
+ def get_paginator(self, queryset, per_page, orphans=0,
+ allow_empty_first_page=True, **kwargs):
"""
Return an instance of the paginator for this view.
"""
- return self.paginator_class(queryset, per_page, orphans=orphans, allow_empty_first_page=allow_empty_first_page)
+ return self.paginator_class(
+ queryset, per_page, orphans=orphans,
+ allow_empty_first_page=allow_empty_first_page, **kwargs)
+
+ def get_paginate_orphans(self):
+ """
+ Returns the maximum number of orphans extend the last page by when
+ paginating.
+ """
+ return self.paginate_orphans
def get_allow_empty(self):
"""
diff --git a/django/views/i18n.py b/django/views/i18n.py
index 00ef224254..c87e3a82db 100644
--- a/django/views/i18n.py
+++ b/django/views/i18n.py
@@ -8,6 +8,8 @@ from django.utils.translation import check_for_language, activate, to_locale, ge
from django.utils.text import javascript_quote
from django.utils.encoding import smart_text
from django.utils.formats import get_format_modules, get_format
+from django.utils._os import upath
+from django.utils.http import is_safe_url
from django.utils import six
def set_language(request):
@@ -21,11 +23,11 @@ def set_language(request):
redirect to the page in the request (the 'next' parameter) without changing
any state.
"""
- next = request.REQUEST.get('next', None)
- if not next:
- next = request.META.get('HTTP_REFERER', None)
- if not next:
- next = '/'
+ next = request.REQUEST.get('next')
+ if not is_safe_url(url=next, host=request.get_host()):
+ next = request.META.get('HTTP_REFERER')
+ if not is_safe_url(url=next, host=request.get_host()):
+ next = '/'
response = http.HttpResponseRedirect(next)
if request.method == 'POST':
lang_code = request.POST.get('language', None)
@@ -99,16 +101,16 @@ function ngettext(singular, plural, count) {
function gettext_noop(msgid) { return msgid; }
function pgettext(context, msgid) {
- var value = gettext(context + '\x04' + msgid);
- if (value.indexOf('\x04') != -1) {
+ var value = gettext(context + '\\x04' + msgid);
+ if (value.indexOf('\\x04') != -1) {
value = msgid;
}
return value;
}
function npgettext(context, singular, plural, count) {
- var value = ngettext(context + '\x04' + singular, context + '\x04' + plural, count);
- if (value.indexOf('\x04') != -1) {
+ var value = ngettext(context + '\\x04' + singular, context + '\\x04' + plural, count);
+ if (value.indexOf('\\x04') != -1) {
value = ngettext(singular, plural, count);
}
return value;
@@ -197,7 +199,7 @@ def javascript_catalog(request, domain='djangojs', packages=None):
# paths of requested packages
for package in packages:
p = importlib.import_module(package)
- path = os.path.join(os.path.dirname(p.__file__), 'locale')
+ path = os.path.join(os.path.dirname(upath(p.__file__)), 'locale')
paths.append(path)
# add the filesystem paths listed in the LOCALE_PATHS setting
paths.extend(list(reversed(settings.LOCALE_PATHS)))
diff --git a/docs/Makefile b/docs/Makefile
index bdf48549a3..f6293a8e7f 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -31,6 +31,7 @@ help:
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
+ @echo " texinfo to make a Texinfo source file"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@@ -116,6 +117,11 @@ man:
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished; the Texinfo files are in $(BUILDDIR)/texinfo."
+
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
diff --git a/docs/_theme/djangodocs/static/djangodocs.css b/docs/_theme/djangodocs/static/djangodocs.css
index 4adb8387cc..4efb7e04f3 100644
--- a/docs/_theme/djangodocs/static/djangodocs.css
+++ b/docs/_theme/djangodocs/static/djangodocs.css
@@ -1,7 +1,7 @@
/*** setup ***/
html { background:#092e20;}
body { font:12px/1.5 Verdana,sans-serif; background:#092e20; color: white;}
-#custom-doc { width:76.54em;*width:74.69em;min-width:995px; max-width:100em; margin:auto; text-align:left; padding-top:16px; margin-top:0;}
+#custom-doc { width:76.54em;*width:74.69em;min-width:995px; max-width:100em; margin:auto; text-align:left; padding-top:16px; margin-top:0;}
#hd { padding: 4px 0 12px 0; }
#bd { background:#234F32; }
#ft { color:#487858; font-size:90%; padding-bottom: 2em; }
@@ -54,7 +54,7 @@ hr { color:#ccc; background-color:#ccc; height:1px; border:0; }
p, ul, dl { margin-top:.6em; margin-bottom:1em; padding-bottom: 0.1em;}
#yui-main div.yui-b img { max-width: 50em; margin-left: auto; margin-right: auto; display: block; }
caption { font-size:1em; font-weight:bold; margin-top:0.5em; margin-bottom:0.5em; margin-left: 2px; text-align: center; }
-blockquote { padding: 0 1em; margin: 1em 0; font:125%/1.2em "Trebuchet MS", sans-serif; color:#234f32; border-left:2px solid #94da3a; }
+blockquote { padding: 0 1em; margin: 1em 0; font:125%/1.2em "Trebuchet MS", sans-serif; color:#234f32; border-left:2px solid #94da3a; }
strong { font-weight: bold; }
em { font-style: italic; }
ins { font-weight: bold; text-decoration: none; }
@@ -111,6 +111,7 @@ dt .literal, table .literal { background:none; }
.note, .admonition { padding-left:65px; background:url(docicons-note.png) .8em .8em no-repeat;}
div.admonition-philosophy { padding-left:65px; background:url(docicons-philosophy.png) .8em .8em no-repeat;}
div.admonition-behind-the-scenes { padding-left:65px; background:url(docicons-behindscenes.png) .8em .8em no-repeat;}
+.admonition.warning { background:url(docicons-warning.png) .8em .8em no-repeat; border:1px solid #ffc83c;}
/*** versoinadded/changes ***/
div.versionadded, div.versionchanged { }
diff --git a/docs/_theme/djangodocs/static/docicons-warning.png b/docs/_theme/djangodocs/static/docicons-warning.png
new file mode 100644
index 0000000000..031b3e782a
--- /dev/null
+++ b/docs/_theme/djangodocs/static/docicons-warning.png
Binary files differ
diff --git a/docs/conf.py b/docs/conf.py
index 433fd679a1..f58e4ecb2e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -14,12 +14,15 @@
from __future__ import unicode_literals
import sys
-import os
+from os.path import abspath, dirname, join
+
+# Make sure we get the version of this copy of Django
+sys.path.insert(1, dirname(dirname(abspath(__file__))))
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext")))
+sys.path.append(abspath(join(dirname(__file__), "_ext")))
# -- General configuration -----------------------------------------------------
@@ -52,11 +55,23 @@ copyright = 'Django Software Foundation and contributors'
# built documents.
#
# The short X.Y version.
-version = '1.5'
+version = '1.6'
# The full version, including alpha/beta/rc tags.
-release = '1.5'
+try:
+ from django import VERSION, get_version
+except ImportError:
+ release = version
+else:
+ def django_release():
+ pep386ver = get_version()
+ if VERSION[3:5] == ('alpha', 0) and 'dev' not in pep386ver:
+ return pep386ver + '.dev'
+ return pep386ver
+
+ release = django_release()
+
# The next version to be released
-django_next_version = '1.6'
+django_next_version = '1.7'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -233,6 +248,16 @@ man_pages = [
]
+# -- Options for Texinfo output ------------------------------------------------
+
+# List of tuples (startdocname, targetname, title, author, dir_entry,
+# description, category, toctree_only)
+texinfo_documents=[(
+ master_doc, "django", "", "", "Django",
+ "Documentation of the Django framework", "Web development", False
+)]
+
+
# -- Options for Epub output ---------------------------------------------------
# Bibliographic Dublin Core info.
diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt
index 872ad254c9..30d452cbe2 100644
--- a/docs/faq/admin.txt
+++ b/docs/faq/admin.txt
@@ -10,8 +10,8 @@ things:
* Set the :setting:`SESSION_COOKIE_DOMAIN` setting in your admin config
file to match your domain. For example, if you're going to
- "http://www.example.com/admin/" in your browser, in
- "myproject.settings" you should set ``SESSION_COOKIE_DOMAIN = 'www.example.com'``.
+ "http://www.example.com/admin/" in your browser, in "myproject.settings" you
+ should set :setting:`SESSION_COOKIE_DOMAIN` = 'www.example.com'.
* Some browsers (Firefox?) don't like to accept cookies from domains that
don't have dots in them. If you're running the admin site on "localhost"
@@ -23,8 +23,9 @@ I can't log in. When I enter a valid username and password, it brings up the log
-----------------------------------------------------------------------------------------------------------------------------------------------------------
If you're sure your username and password are correct, make sure your user
-account has ``is_active`` and ``is_staff`` set to True. The admin site only
-allows access to users with those two fields both set to True.
+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?
-------------------------------------------------------------------
@@ -64,9 +65,10 @@ My "list_filter" contains a ManyToManyField, but the filter doesn't display.
Django won't bother displaying the filter for a ``ManyToManyField`` if there
are fewer than two related objects.
-For example, if your ``list_filter`` includes ``sites``, and there's only one
-site in your database, it won't display a "Site" filter. In that case,
-filtering by site would be meaningless.
+For example, if your :attr:`~django.contrib.admin.ModelAdmin.list_filter`
+includes :doc:`sites </ref/contrib/sites>`, and there's only one site in your
+database, it won't display a "Site" filter. In that case, filtering by site
+would be meaningless.
Some objects aren't appearing in the admin.
-------------------------------------------
@@ -85,9 +87,10 @@ How can I customize the functionality of the admin interface?
You've got several options. If you want to piggyback on top of an add/change
form that Django automatically generates, you can attach arbitrary JavaScript
-modules to the page via the model's ``class Admin`` ``js`` parameter. That
-parameter is a list of URLs, as strings, pointing to JavaScript modules that
-will be included within the admin form via a ``<script>`` tag.
+modules to the page via the model's class Admin :ref:`js parameter
+<modeladmin-media-definitions>`. That parameter is a list of URLs, as strings,
+pointing to JavaScript modules that will be included within the admin form via
+a ``<script>`` tag.
If you want more flexibility than simply tweaking the auto-generated forms,
feel free to write custom views for the admin. The admin is powered by Django
diff --git a/docs/faq/general.txt b/docs/faq/general.txt
index 659a8c5ad9..dc569840d1 100644
--- a/docs/faq/general.txt
+++ b/docs/faq/general.txt
@@ -68,7 +68,7 @@ Who's behind this?
Django was originally developed at World Online, the Web department of a
newspaper in Lawrence, Kansas, USA. Django's now run by an international team of
volunteers; you can read all about them over at the :doc:`list of committers
-</internals/committers>`
+</internals/committers>`.
Which sites use Django?
-----------------------
diff --git a/docs/faq/install.txt b/docs/faq/install.txt
index a772a379d5..a92c9d87ea 100644
--- a/docs/faq/install.txt
+++ b/docs/faq/install.txt
@@ -18,7 +18,7 @@ What are Django's prerequisites?
Django requires Python, specifically Python 2.6.5 - 2.7.x. No other Python
libraries are required for basic Django usage. Django 1.5 also has
-experimental support for Python 3.2 and above.
+experimental support for Python 3.2.3 and above.
For a development environment -- if you just want to experiment with Django --
you don't need to have a separate Web server installed; Django comes with its
@@ -69,14 +69,14 @@ Django version Python versions
1.2 2.4, 2.5, 2.6, 2.7
1.3 2.4, 2.5, 2.6, 2.7
**1.4** **2.5, 2.6, 2.7**
-*1.5 (future)* *2.6, 2.7* and *3.2, 3.3 (experimental)*
+*1.5 (future)* *2.6, 2.7* and *3.2.3, 3.3 (experimental)*
============== ===============
Can I use Django with Python 3?
-------------------------------
-Django 1.5 introduces experimental support for Python 3.2 and 3.3. However, we
-don't yet suggest that you use Django and Python 3 in production.
+Django 1.5 introduces experimental support for Python 3.2.3 and above. However,
+we don't yet suggest that you use Django and Python 3 in production.
Python 3 support should be considered a "preview". It's offered to bootstrap
the transition of the Django ecosystem to Python 3, and to help you start
diff --git a/docs/faq/troubleshooting.txt b/docs/faq/troubleshooting.txt
index f984be4bf5..da286cf4e2 100644
--- a/docs/faq/troubleshooting.txt
+++ b/docs/faq/troubleshooting.txt
@@ -5,6 +5,11 @@ Troubleshooting
This page contains some advice about errors and problems commonly encountered
during the development of Django applications.
+.. _troubleshooting-django-admin-py:
+
+Problems running django-admin.py
+================================
+
"command not found: django-admin.py"
------------------------------------
@@ -13,4 +18,34 @@ installed Django via ``python setup.py``. If it's not on your path, you can
find it in ``site-packages/django/bin``, where ``site-packages`` is a directory
within your Python installation. Consider symlinking to :doc:`django-admin.py
</ref/django-admin>` from some place on your path, such as
-:file:`/usr/local/bin`. \ No newline at end of file
+:file:`/usr/local/bin`.
+
+Script name may differ in distribution packages
+-----------------------------------------------
+
+If you installed Django using a Linux distribution's package manager
+(e.g. ``apt-get`` or ``yum``) ``django-admin.py`` may have been renamed to
+``django-admin``; use that instead.
+
+Mac OS X permissions
+--------------------
+
+If you're using Mac OS X, you may see the message "permission denied" when
+you try to run ``django-admin.py``. This is because, on Unix-based systems like
+OS X, a file must be marked as "executable" before it can be run as a program.
+To do this, open Terminal.app and navigate (using the ``cd`` command) to the
+directory where :doc:`django-admin.py </ref/django-admin>` is installed, then
+run the command ``sudo chmod +x django-admin.py``.
+
+Running virtualenv on Windows
+-----------------------------
+
+If you used virtualenv_ to :ref:`install Django <installing-official-release>`
+on Windows, you may get an ``ImportError`` when you try to run
+``django-admin.py``. This is because Windows does not run the
+Python interpreter from your virtual environment unless you invoke it
+directly. Instead, prefix all commands that use .py files with ``python`` and
+use the full path to the file, like so:
+``python C:\pythonXY\Scripts\django-admin.py``.
+
+.. _virtualenv: http://www.virtualenv.org/
diff --git a/docs/howto/custom-file-storage.txt b/docs/howto/custom-file-storage.txt
index 5f1dae17ef..090cc089cb 100644
--- a/docs/howto/custom-file-storage.txt
+++ b/docs/howto/custom-file-storage.txt
@@ -27,9 +27,9 @@ You'll need to follow these steps:
option = settings.CUSTOM_STORAGE_OPTIONS
...
-#. Your storage class must implement the ``_open()`` and ``_save()`` methods,
- along with any other methods appropriate to your storage class. See below for
- more on these methods.
+#. Your storage class must implement the :meth:`_open()` and :meth:`_save()`
+ methods, along with any other methods appropriate to your storage class. See
+ below for more on these methods.
In addition, if your class provides local file storage, it must override
the ``path()`` method.
@@ -46,8 +46,7 @@ Your custom storage system may override any of the storage methods explained in
You'll also usually want to use hooks specifically designed for custom storage
objects. These are:
-``_open(name, mode='rb')``
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. method:: _open(name, mode='rb')
**Required**.
@@ -56,8 +55,7 @@ uses to open the file. This must return a ``File`` object, though in most cases,
you'll want to return some subclass here that implements logic specific to the
backend storage system.
-``_save(name, content)``
-~~~~~~~~~~~~~~~~~~~~~~~~
+.. method:: _save(name, content)
Called by ``Storage.save()``. The ``name`` will already have gone through
``get_valid_name()`` and ``get_available_name()``, and the ``content`` will be a
@@ -67,8 +65,8 @@ Should return the actual name of name of the file saved (usually the ``name``
passed in, but if the storage needs to change the file name return the new name
instead).
-``get_valid_name(name)``
-~~~~~~~~~~~~~~~~~~~~~~~~
+.. method:: get_valid_name(name)
+
Returns a filename suitable for use with the underlying storage system. The
``name`` argument passed to this method is the original filename sent to the
@@ -78,8 +76,7 @@ how non-standard characters are converted to safe filenames.
The code provided on ``Storage`` retains only alpha-numeric characters, periods
and underscores from the original filename, removing everything else.
-``get_available_name(name)``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. method:: get_available_name(name)
Returns a filename that is available in the storage mechanism, possibly taking
the provided filename into account. The ``name`` argument passed to this method
diff --git a/docs/howto/deployment/wsgi/modwsgi.txt b/docs/howto/deployment/wsgi/modwsgi.txt
index 7f68485dff..ead04a4643 100644
--- a/docs/howto/deployment/wsgi/modwsgi.txt
+++ b/docs/howto/deployment/wsgi/modwsgi.txt
@@ -88,12 +88,20 @@ Using mod_wsgi daemon mode
==========================
"Daemon mode" is the recommended mode for running mod_wsgi (on non-Windows
-platforms). See the `official mod_wsgi documentation`_ for details on setting
-up daemon mode. The only change required to the above configuration if you use
-daemon mode is that you can't use ``WSGIPythonPath``; instead you should use
-the ``python-path`` option to ``WSGIDaemonProcess``, for example::
+platforms). To create the required daemon process group and delegate the
+Django instance to run in it, you will need to add appropriate
+``WSGIDaemonProcess`` and ``WSGIProcessGroup`` directives. A further change
+required to the above configuration if you use daemon mode is that you can't
+use ``WSGIPythonPath``; instead you should use the ``python-path`` option to
+``WSGIDaemonProcess``, for example::
WSGIDaemonProcess example.com python-path=/path/to/mysite.com:/path/to/venv/lib/python2.7/site-packages
+ WSGIProcessGroup example.com
+
+See the official mod_wsgi documentation for `details on setting up daemon
+mode`_.
+
+.. _details on setting up daemon mode: http://code.google.com/p/modwsgi/wiki/QuickConfigurationGuide#Delegation_To_Daemon_Process
.. _serving-files:
diff --git a/docs/index.txt b/docs/index.txt
index 5055edf7e7..9fea8ff3f2 100644
--- a/docs/index.txt
+++ b/docs/index.txt
@@ -44,7 +44,12 @@ Are you new to Django or to programming? This is the place to start!
:doc:`Part 1 <intro/tutorial01>` |
:doc:`Part 2 <intro/tutorial02>` |
:doc:`Part 3 <intro/tutorial03>` |
- :doc:`Part 4 <intro/tutorial04>`
+ :doc:`Part 4 <intro/tutorial04>` |
+ :doc:`Part 5 <intro/tutorial05>`
+
+* **Advanced Tutorials:**
+ :doc:`How to write reusable apps <intro/reusable-apps>` |
+ :doc:`Writing your first patch for Django <intro/contributing>`
The model layer
===============
diff --git a/docs/internals/contributing/writing-code/submitting-patches.txt b/docs/internals/contributing/writing-code/submitting-patches.txt
index b51ac0d906..a90dc32605 100644
--- a/docs/internals/contributing/writing-code/submitting-patches.txt
+++ b/docs/internals/contributing/writing-code/submitting-patches.txt
@@ -52,8 +52,15 @@ and time availability), claim it by following these steps:
page,
2. then click "Submit changes."
+.. note::
+ The Django software foundation requests that anyone contributing more than
+ a trivial patch to Django sign and submit a `Contributor License
+ Agreement`_, this ensures that the Django Software Foundation has clear
+ license to all contributions allowing for a clear license for all users.
+
.. _Create an account: https://www.djangoproject.com/accounts/register/
.. _password reset page: https://www.djangoproject.com/accounts/password/reset/
+.. _Contributor License Agreement: https://www.djangoproject.com/foundation/cla/
Ticket claimers' responsibility
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt
index a828b06b36..4e702ff83e 100644
--- a/docs/internals/contributing/writing-code/unit-tests.txt
+++ b/docs/internals/contributing/writing-code/unit-tests.txt
@@ -9,7 +9,8 @@ The tests cover:
* Models and the database API (``tests/modeltests``),
* Everything else in core Django code (``tests/regressiontests``),
-* :ref:`contrib-apps` (``django/contrib/<app>/tests``).
+* :ref:`contrib-apps` (``django/contrib/<app>/tests`` or
+ ``tests/regressiontests/<app>_...``).
We appreciate any and all contributions to the test suite!
@@ -38,6 +39,8 @@ with this sample ``settings`` module, ``cd`` into the Django
If you get an ``ImportError: No module named django.contrib`` error,
you need to add your install of Django to your ``PYTHONPATH``.
+.. _running-unit-tests-settings:
+
Using another ``settings`` module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -133,6 +136,8 @@ Then, run the tests normally, for example:
./runtests.py --settings=test_sqlite admin_inlines
+.. _running-unit-tests-dependencies:
+
Running all the tests
~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 10bbfe1a91..414da30ff8 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -140,6 +140,11 @@ these changes.
removed. In its place use
:class:`~django.contrib.staticfiles.handlers.StaticFilesHandler`.
+* The template tags library ``adminmedia`` and the template tag ``{%
+ admin_media_prefix %}`` will be removed in favor of the generic static files
+ handling. (This is faster than the usual deprecation path; see the
+ :doc:`Django 1.4 release notes</releases/1.4>`.)
+
* The :ttag:`url` and :ttag:`ssi` template tags will be
modified so that the first argument to each tag is a template variable, not
an implied string. In 1.4, this behavior is provided by a version of the tag
@@ -232,10 +237,6 @@ these changes.
:setting:`LOGGING` setting should include this filter explicitly if
it is desired.
-* The template tag
- :func:`django.contrib.admin.templatetags.adminmedia.admin_media_prefix`
- will be removed in favor of the generic static files handling.
-
* The builtin truncation functions :func:`django.utils.text.truncate_words`
and :func:`django.utils.text.truncate_html_words` will be removed in
favor of the ``django.utils.text.Truncator`` class.
@@ -293,6 +294,14 @@ these changes.
* The ``AUTH_PROFILE_MODULE`` setting, and the ``get_profile()`` method on
the User model, will be removed.
+* The ``cleanup`` management command will be removed. It's replaced by
+ ``clearsessions``.
+
+* The ``daily_cleanup.py`` script will be removed.
+
+* The ``depth`` keyword argument will be removed from
+ :meth:`~django.db.models.query.QuerySet.select_related`.
+
2.0
---
diff --git a/docs/intro/contributing.txt b/docs/intro/contributing.txt
new file mode 100644
index 0000000000..a343814c02
--- /dev/null
+++ b/docs/intro/contributing.txt
@@ -0,0 +1,580 @@
+===================================
+Writing your first patch for Django
+===================================
+
+Introduction
+============
+
+Interested in giving back to the community a little? Maybe you've found a bug
+in Django that you'd like to see fixed, or maybe there's a small feature you
+want added.
+
+Contributing back to Django itself is the best way to see your own concerns
+addressed. This may seem daunting at first, but it's really pretty simple.
+We'll walk you through the entire process, so you can learn by example.
+
+Who's this tutorial for?
+------------------------
+
+For this tutorial, we expect that you have at least a basic understanding of
+how Django works. This means you should be comfortable going through the
+existing tutorials on :doc:`writing your first Django app</intro/tutorial01>`.
+In addition, you should have a good understanding of Python itself. But if you
+don't, `Dive Into Python`__ is a fantastic (and free) online book for beginning
+Python programmers.
+
+Those of you who are unfamiliar with version control systems and Trac will find
+that this tutorial and its links include just enough information to get started.
+However, you'll probably want to read some more about these different tools if
+you plan on contributing to Django regularly.
+
+For the most part though, this tutorial tries to explain as much as possible,
+so that it can be of use to the widest audience.
+
+.. admonition:: Where to get help:
+
+ If you're having trouble going through this tutorial, please post a message
+ to `django-developers`__ or drop by `#django-dev on irc.freenode.net`__ to
+ chat with other Django users who might be able to help.
+
+__ http://diveintopython.net/toc/index.html
+__ http://groups.google.com/group/django-developers
+__ irc://irc.freenode.net/django-dev
+
+What does this tutorial cover?
+------------------------------
+
+We'll be walking you through contributing a patch to Django for the first time.
+By the end of this tutorial, you should have a basic understanding of both the
+tools and the processes involved. Specifically, we'll be covering the following:
+
+* Installing Git.
+* How to download a development copy of Django.
+* Running Django's test suite.
+* Writing a test for your patch.
+* Writing the code for your patch.
+* Testing your patch.
+* Generating a patch file for your changes.
+* Where to look for more information.
+
+Once you're done with the tutorial, you can look through the rest of
+:doc:`Django's documentation on contributing</internals/contributing/index>`.
+It contains lots of great information and is a must read for anyone who'd like
+to become a regular contributor to Django. If you've got questions, it's
+probably got the answers.
+
+Installing Git
+==============
+
+For this tutorial, you'll need Git installed to download the current
+development version of Django and to generate patch files for the changes you
+make.
+
+To check whether or not you have Git installed, enter ``git`` into the command
+line. If you get messages saying that this command could be found, you'll have
+to download and install it, see `Git's download page`__.
+
+If you're not that familiar with Git, you can always find out more about its
+commands (once it's installed) by typing ``git help`` into the command line.
+
+__ http://git-scm.com/download
+
+Getting a copy of Django's development version
+==============================================
+
+The first step to contributing to Django is to get a copy of the source code.
+From the command line, use the ``cd`` command to navigate to the directory
+where you'll want your local copy of Django to live.
+
+Download the Django source code repository using the following command::
+
+ git clone https://github.com/django/django.git
+
+.. note::
+
+ For users who wish to use `virtualenv`__, you can use::
+
+ pip install -e /path/to/your/local/clone/django/
+
+ to link your cloned checkout into a virtual environment. This is a great
+ option to isolate your development copy of Django from the rest of your
+ system and avoids potential package conflicts.
+
+__ http://www.virtualenv.org
+
+Rolling back to a previous revision of Django
+=============================================
+
+For this tutorial, we'll be using `ticket #17549`__ as a case study, so we'll
+rewind Django's version history in git to before that ticket's patch was
+applied. This will allow us to go through all of the steps involved in writing
+that patch from scratch, including running Django's test suite.
+
+**Keep in mind that while we'll be using an older revision of Django's trunk
+for the purposes of the tutorial below, you should always use the current
+development revision of Django when working on your own patch for a ticket!**
+
+.. note::
+
+ The patch for this ticket was written by Ulrich Petri, and it was applied
+ to Django as `commit ac2052ebc84c45709ab5f0f25e685bf656ce79bc`__.
+ Consequently, we'll be using the revision of Django just prior to that,
+ `commit 39f5bc7fc3a4bb43ed8a1358b17fe0521a1a63ac`__.
+
+__ https://code.djangoproject.com/ticket/17549
+__ https://github.com/django/django/commit/ac2052ebc84c45709ab5f0f25e685bf656ce79bc
+__ https://github.com/django/django/commit/39f5bc7fc3a4bb43ed8a1358b17fe0521a1a63ac
+
+Navigate into Django's root directory (that's the one that contains ``django``,
+``docs``, ``tests``, ``AUTHORS``, etc.). You can then check out the older
+revision of Django that we'll be using in the tutorial below::
+
+ git checkout 39f5bc7fc3a4bb43ed8a1358b17fe0521a1a63ac
+
+Running Django's test suite for the first time
+==============================================
+
+When contributing to Django it's very important that your code changes don't
+introduce bugs into other areas of Django. One way to check that Django still
+works after you make your changes is by running Django's test suite. If all
+the tests still pass, then you can be reasonably sure that your changes
+haven't completely broken Django. If you've never run Django's test suite
+before, it's a good idea to run it once beforehand just to get familiar with
+what its output is supposed to look like.
+
+We can run the test suite by simply ``cd``-ing into the Django ``tests/``
+directory and, if you're using GNU/Linux, Mac OS X or some other flavor of
+Unix, run::
+
+ PYTHONPATH=.. python runtests.py --settings=test_sqlite
+
+If you're on Windows, the above should work provided that you are using
+"Git Bash" provided by the default Git install. GitHub has a `nice tutorial`__.
+
+__ https://help.github.com/articles/set-up-git#platform-windows
+
+.. note::
+
+ If you're using ``virtualenv``, you can omit ``PYTHONPATH=..`` when running
+ the tests. This instructs Python to look for Django in the parent directory
+ of ``tests``. ``virtualenv`` puts your copy of Django on the ``PYTHONPATH``
+ automatically.
+
+Now sit back and relax. Django's entire test suite has over 4800 different
+tests, so it can take anywhere from 5 to 15 minutes to run, depending on the
+speed of your computer.
+
+While Django's test suite is running, you'll see a stream of characters
+representing the status of each test as it's run. ``E`` indicates that an error
+was raised during a test, and ``F`` indicates that a test's assertions failed.
+Both of these are considered to be test failures. Meanwhile, ``x`` and ``s``
+indicated expected failures and skipped tests, respectively. Dots indicate
+passing tests.
+
+Skipped tests are typically due to missing external libraries required to run
+the test; see :ref:`running-unit-tests-dependencies` for a list of dependencies
+and be sure to install any for tests related to the changes you are making (we
+won't need any for this tutorial).
+
+Once the tests complete, you should be greeted with a message informing you
+whether the test suite passed or failed. Since you haven't yet made any changes
+to Django's code, the entire test suite **should** pass. If you get failures or
+errors make sure you've followed all of the previous steps properly. See
+:ref:`running-unit-tests` for more information.
+
+Note that the latest Django trunk may not always be stable. When developing
+against trunk, you can check `Django's continuous integration builds`__ to
+determine if the failures are specific to your machine or if they are also
+present in Django's official builds. If you click to view a particular build,
+you can view the "Configuration Matrix" which shows failures broken down by
+Python version and database backend.
+
+__ http://ci.djangoproject.com/
+
+.. note::
+
+ For this tutorial and the ticket we're working on, testing against SQLite
+ is sufficient, however, it's possible (and sometimes necessary) to
+ :ref:`run the tests using a different database
+ <running-unit-tests-settings>`.
+
+Writing some tests for your ticket
+==================================
+
+In most cases, for a patch to be accepted into Django it has to include tests.
+For bug fix patches, this means writing a regression test to ensure that the
+bug is never reintroduced into Django later on. A regression test should be
+written in such a way that it will fail while the bug still exists and pass
+once the bug has been fixed. For patches containing new features, you'll need
+to include tests which ensure that the new features are working correctly.
+They too should fail when the new feature is not present, and then pass once it
+has been implemented.
+
+A good way to do this is to write your new tests first, before making any
+changes to the code. This style of development is called
+`test-driven development`__ and can be applied to both entire projects and
+single patches. After writing your tests, you then run them to make sure that
+they do indeed fail (since you haven't fixed that bug or added that feature
+yet). If your new tests don't fail, you'll need to fix them so that they do.
+After all, a regression test that passes regardless of whether a bug is present
+is not very helpful at preventing that bug from reoccurring down the road.
+
+Now for our hands-on example.
+
+__ http://en.wikipedia.org/wiki/Test-driven_development
+
+Writing some tests for ticket #17549
+------------------------------------
+
+`Ticket #17549`__ describes the following, small feature addition:
+
+ It's useful for URLField to give you a way to open the URL; otherwise you
+ might as well use a CharField.
+
+In order to resolve this ticket, we'll add a ``render`` method to the
+``AdminURLFieldWidget`` in order to display a clickable link above the input
+widget. Before we make those changes though, we're going to write a couple
+tests to verify that our modification functions correctly and continues to
+function correctly in the future.
+
+Navigate to Django's ``tests/regressiontests/admin_widgets/`` folder and
+open the ``tests.py`` file. Add the following code on line 269 right before the
+``AdminFileWidgetTest`` class::
+
+ class AdminURLWidgetTest(DjangoTestCase):
+ def test_render(self):
+ w = widgets.AdminURLFieldWidget()
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', '')),
+ '<input class="vURLField" name="test" type="text" />'
+ )
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', 'http://example.com')),
+ '<p class="url">Currently:<a href="http://example.com">http://example.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com" /></p>'
+ )
+
+ def test_render_idn(self):
+ w = widgets.AdminURLFieldWidget()
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', 'http://example-äüö.com')),
+ '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com">http://example-äüö.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com" /></p>'
+ )
+
+ def test_render_quoting(self):
+ w = widgets.AdminURLFieldWidget()
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', 'http://example.com/<sometag>some text</sometag>')),
+ '<p class="url">Currently:<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com/<sometag>some text</sometag>" /></p>'
+ )
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>')),
+ '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com/<sometag>some text</sometag>" /></p>'
+ )
+
+The new tests check to see that the ``render`` method we'll be adding works
+correctly in a couple different situations.
+
+.. admonition:: But this testing thing looks kinda hard...
+
+ If you've never had to deal with tests before, they can look a little hard
+ to write at first glance. Fortunately, testing is a *very* big subject in
+ computer programming, so there's lots of information out there:
+
+ * A good first look at writing tests for Django can be found in the
+ documentation on :doc:`Testing Django applications</topics/testing/>`.
+ * Dive Into Python (a free online book for beginning Python developers)
+ includes a great `introduction to Unit Testing`__.
+ * After reading those, if you want something a little meatier to sink
+ your teeth into, there's always the `Python unittest documentation`__.
+
+__ https://code.djangoproject.com/ticket/17549
+__ http://diveintopython.net/unit_testing/index.html
+__ http://docs.python.org/library/unittest.html
+
+Running your new test
+---------------------
+
+Remember that we haven't actually made any modifications to
+``AdminURLFieldWidget`` yet, so our tests are going to fail. Let's run all the
+tests in the ``model_forms_regress`` folder to make sure that's really what
+happens. From the command line, ``cd`` into the Django ``tests/`` directory
+and run::
+
+ PYTHONPATH=.. python runtests.py --settings=test_sqlite admin_widgets
+
+If the tests ran correctly, you should see three failures corresponding to each
+of the test methods we added. If all of the tests passed, then you'll want to
+make sure that you added the new test shown above to the appropriate folder and
+class.
+
+Writing the code for your ticket
+================================
+
+Next we'll be adding the functionality described in `ticket #17549`__ to Django.
+
+Writing the code for ticket #17549
+----------------------------------
+
+Navigate to the ``django/django/contrib/admin/`` folder and open the
+``widgets.py`` file. Find the ``AdminURLFieldWidget`` class on line 302 and add
+the following ``render`` method after the existing ``__init__`` method::
+
+ def render(self, name, value, attrs=None):
+ html = super(AdminURLFieldWidget, self).render(name, value, attrs)
+ if value:
+ value = force_text(self._format_value(value))
+ final_attrs = {'href': mark_safe(smart_urlquote(value))}
+ html = format_html(
+ '<p class="url">{0} <a {1}>{2}</a><br />{3} {4}</p>',
+ _('Currently:'), flatatt(final_attrs), value,
+ _('Change:'), html
+ )
+ return html
+
+Verifying your test now passes
+------------------------------
+
+Once you're done modifying Django, we need to make sure that the tests we wrote
+earlier pass, so we can see whether the code we wrote above is working
+correctly. To run the tests in the ``admin_widgets`` folder, ``cd`` into the
+Django ``tests/`` directory and run::
+
+ PYTHONPATH=.. python runtests.py --settings=test_sqlite admin_widgets
+
+Oops, good thing we wrote those tests! You should still see 3 failures with
+the following exception::
+
+ NameError: global name 'smart_urlquote' is not defined
+
+We forgot to add the import for that method. Go ahead and add the
+``smart_urlquote`` import at the end of line 13 of
+``django/contrib/admin/widgets.py`` so it looks as follows::
+
+ from django.utils.html import escape, format_html, format_html_join, smart_urlquote
+
+Re-run the tests and everything should pass. If it doesn't, make sure you
+correctly modified the ``AdminURLFieldWidget`` class as shown above and
+copied the new tests correctly.
+
+__ https://code.djangoproject.com/ticket/17549
+
+Running Django's test suite for the second time
+===============================================
+
+Once you've verified that your patch and your test are working correctly, it's
+a good idea to run the entire Django test suite just to verify that your change
+hasn't introduced any bugs into other areas of Django. While successfully
+passing the entire test suite doesn't guarantee your code is bug free, it does
+help identify many bugs and regressions that might otherwise go unnoticed.
+
+To run the entire Django test suite, ``cd`` into the Django ``tests/``
+directory and run::
+
+ PYTHONPATH=.. python runtests.py --settings=test_sqlite
+
+As long as you don't see any failures, you're good to go. Note that this fix
+also made a `small CSS change`__ to format the new widget. You can make the
+change if you'd like, but we'll skip it for now in the interest of brevity.
+
+__ https://github.com/django/django/commit/ac2052ebc84c45709ab5f0f25e685bf656ce79bc#diff-0
+
+Writing Documentation
+=====================
+
+This is a new feature, so it should be documented. Add the following on line
+925 of ``django/docs/ref/models/fields.txt`` beneath the existing docs for
+``URLField``::
+
+ .. versionadded:: 1.5
+
+ The current value of the field will be displayed as a clickable link above the
+ input widget.
+
+For more information on writing documentation, including an explanation of what
+the ``versionadded`` bit is all about, see
+:doc:`/internals/contributing/writing-documentation`. That page also includes
+an explanation of how to build a copy of the documentation locally, so you can
+preview the HTML that will be generated.
+
+Generating a patch for your changes
+===================================
+
+Now it's time to generate a patch file that can be uploaded to Trac or applied
+to another copy of Django. To get a look at the content of your patch, run the
+following command::
+
+ git diff
+
+This will display the differences between your current copy of Django (with
+your changes) and the revision that you initially checked out earlier in the
+tutorial.
+
+Once you're done looking at the patch, hit the ``q`` key to exit back to the
+command line. If the patch's content looked okay, you can run the following
+command to save the patch file to your current working directory::
+
+ git diff > 17549.diff
+
+You should now have a file in the root Django directory called ``17549.diff``.
+This patch file contains all your changes and should look this:
+
+.. code-block:: diff
+
+ diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
+ index 1e0bc2d..9e43a10 100644
+ --- a/django/contrib/admin/widgets.py
+ +++ b/django/contrib/admin/widgets.py
+ @@ -10,7 +10,7 @@ from django.contrib.admin.templatetags.admin_static import static
+ from django.core.urlresolvers import reverse
+ from django.forms.widgets import RadioFieldRenderer
+ from django.forms.util import flatatt
+ -from django.utils.html import escape, format_html, format_html_join
+ +from django.utils.html import escape, format_html, format_html_join, smart_urlquote
+ from django.utils.text import Truncator
+ from django.utils.translation import ugettext as _
+ from django.utils.safestring import mark_safe
+ @@ -306,6 +306,18 @@ class AdminURLFieldWidget(forms.TextInput):
+ final_attrs.update(attrs)
+ super(AdminURLFieldWidget, self).__init__(attrs=final_attrs)
+
+ + def render(self, name, value, attrs=None):
+ + html = super(AdminURLFieldWidget, self).render(name, value, attrs)
+ + if value:
+ + value = force_text(self._format_value(value))
+ + final_attrs = {'href': mark_safe(smart_urlquote(value))}
+ + html = format_html(
+ + '<p class="url">{0} <a {1}>{2}</a><br />{3} {4}</p>',
+ + _('Currently:'), flatatt(final_attrs), value,
+ + _('Change:'), html
+ + )
+ + return html
+ +
+ class AdminIntegerFieldWidget(forms.TextInput):
+ class_name = 'vIntegerField'
+
+ diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
+ index 809d56e..d44f85f 100644
+ --- a/docs/ref/models/fields.txt
+ +++ b/docs/ref/models/fields.txt
+ @@ -922,6 +922,10 @@ Like all :class:`CharField` subclasses, :class:`URLField` takes the optional
+ :attr:`~CharField.max_length`argument. If you don't specify
+ :attr:`~CharField.max_length`, a default of 200 is used.
+
+ +.. versionadded:: 1.5
+ +
+ +The current value of the field will be displayed as a clickable link above the
+ +input widget.
+
+ Relationship fields
+ ===================
+ diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
+ index 4b11543..94acc6d 100644
+ --- a/tests/regressiontests/admin_widgets/tests.py
+ +++ b/tests/regressiontests/admin_widgets/tests.py
+ @@ -265,6 +265,35 @@ class AdminSplitDateTimeWidgetTest(DjangoTestCase):
+ '<p class="datetime">Datum: <input value="01.12.2007" type="text" class="vDateField" name="test_0" size="10" /><br />Zeit: <input value="09:30:00" type="text" class="vTimeField" name="test_1" size="8" /></p>',
+ )
+
+ +class AdminURLWidgetTest(DjangoTestCase):
+ + def test_render(self):
+ + w = widgets.AdminURLFieldWidget()
+ + self.assertHTMLEqual(
+ + conditional_escape(w.render('test', '')),
+ + '<input class="vURLField" name="test" type="text" />'
+ + )
+ + self.assertHTMLEqual(
+ + conditional_escape(w.render('test', 'http://example.com')),
+ + '<p class="url">Currently:<a href="http://example.com">http://example.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com" /></p>'
+ + )
+ +
+ + def test_render_idn(self):
+ + w = widgets.AdminURLFieldWidget()
+ + self.assertHTMLEqual(
+ + conditional_escape(w.render('test', 'http://example-äüö.com')),
+ + '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com">http://example-äüö.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com" /></p>'
+ + )
+ +
+ + def test_render_quoting(self):
+ + w = widgets.AdminURLFieldWidget()
+ + self.assertHTMLEqual(
+ + conditional_escape(w.render('test', 'http://example.com/<sometag>some text</sometag>')),
+ + '<p class="url">Currently:<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com/<sometag>some text</sometag>" /></p>'
+ + )
+ + self.assertHTMLEqual(
+ + conditional_escape(w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>')),
+ + '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com/<sometag>some text</sometag>" /></p>'
+ + )
+
+ class AdminFileWidgetTest(DjangoTestCase):
+ def test_render(self):
+
+So what do I do next?
+=====================
+
+Congratulations, you've generated your very first Django patch! Now that you've
+got that under your belt, you can put those skills to good use by helping to
+improve Django's codebase. Generating patches and attaching them to Trac
+tickets is useful, however, since we are using git - adopting a more :doc:`git
+oriented workflow </internals/contributing/writing-code/working-with-git>` is
+recommended.
+
+Since we never committed our changes locally, perform the following to get your
+git branch back to a good starting point::
+
+ git reset --hard HEAD
+ git checkout master
+
+More information for new contributors
+-------------------------------------
+
+Before you get too into writing patches for Django, there's a little more
+information on contributing that you should probably take a look at:
+
+* You should make sure to read Django's documentation on
+ :doc:`claiming tickets and submitting patches
+ </internals/contributing/writing-code/submitting-patches>`.
+ It covers Trac etiquette, how to claim tickets for yourself, expected
+ coding style for patches, and many other important details.
+* First time contributors should also read Django's :doc:`documentation
+ for first time contributors</internals/contributing/new-contributors/>`.
+ It has lots of good advice for those of us who are new to helping out
+ with Django.
+* After those, if you're still hungry for more information about
+ contributing, you can always browse through the rest of
+ :doc:`Django's documentation on contributing</internals/contributing/index>`.
+ It contains a ton of useful information and should be your first source
+ for answering any questions you might have.
+
+Finding your first real ticket
+------------------------------
+
+Once you've looked through some of that information, you'll be ready to go out
+and find a ticket of your own to write a patch for. Pay special attention to
+tickets with the "easy pickings" criterion. These tickets are often much
+simpler in nature and are great for first time contributors. Once you're
+familiar with contributing to Django, you can move on to writing patches for
+more difficult and complicated tickets.
+
+If you just want to get started already (and nobody would blame you!), try
+taking a look at the list of `easy tickets that need patches`__ and the
+`easy tickets that have patches which need improvement`__. If you're familiar
+with writing tests, you can also look at the list of
+`easy tickets that need tests`__. Just remember to follow the guidelines about
+claiming tickets that were mentioned in the link to Django's documentation on
+:doc:`claiming tickets and submitting patches
+</internals/contributing/writing-code/submitting-patches>`.
+
+__ https://code.djangoproject.com/query?status=new&status=reopened&has_patch=0&easy=1&col=id&col=summary&col=status&col=owner&col=type&col=milestone&order=priority
+__ https://code.djangoproject.com/query?status=new&status=reopened&needs_better_patch=1&easy=1&col=id&col=summary&col=status&col=owner&col=type&col=milestone&order=priority
+__ https://code.djangoproject.com/query?status=new&status=reopened&needs_tests=1&easy=1&col=id&col=summary&col=status&col=owner&col=type&col=milestone&order=priority
+
+What's next?
+------------
+
+After a ticket has a patch, it needs to be reviewed by a second set of eyes.
+After uploading a patch or submitting a pull request, be sure to update the
+ticket metadata by setting the flags on the ticket to say "has patch",
+"doesn't need tests", etc, so others can find it for review. Contributing
+doesn't necessarily always mean writing a patch from scratch. Reviewing
+existing patches is also a very helpful contribution. See
+:doc:`/internals/contributing/triaging-tickets` for details.
diff --git a/docs/intro/index.txt b/docs/intro/index.txt
index 19290a53c6..ea6a3c4d29 100644
--- a/docs/intro/index.txt
+++ b/docs/intro/index.txt
@@ -6,31 +6,34 @@ place: read this material to quickly get up and running.
.. toctree::
:maxdepth: 1
-
+
overview
install
tutorial01
tutorial02
tutorial03
tutorial04
+ tutorial05
+ reusable-apps
whatsnext
-
+ contributing
+
.. seealso::
If you're new to Python_, you might want to start by getting an idea of what
the language is like. Django is 100% Python, so if you've got minimal
comfort with Python you'll probably get a lot more out of Django.
-
+
If you're new to programming entirely, you might want to start with this
`list of Python resources for non-programmers`_
-
+
If you already know a few other languages and want to get up to speed with
Python quickly, we recommend `Dive Into Python`_ (also available in a
`dead-tree version`_). If that's not quite your style, there are quite
a few other `books about Python`_.
-
+
.. _python: http://python.org/
.. _list of Python resources for non-programmers: http://wiki.python.org/moin/BeginnersGuide/NonProgrammers
.. _dive into python: http://diveintopython.net/
.. _dead-tree version: http://www.amazon.com/exec/obidos/ASIN/1590593561/ref=nosim/jacobian20
- .. _books about Python: http://wiki.python.org/moin/PythonBooks \ No newline at end of file
+ .. _books about Python: http://wiki.python.org/moin/PythonBooks
diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt
index 4d5a86f82b..ba49e3ccf2 100644
--- a/docs/intro/overview.txt
+++ b/docs/intro/overview.txt
@@ -257,8 +257,7 @@ lookup and function calls.
Note ``{{ article.pub_date|date:"F j, Y" }}`` uses a Unix-style "pipe" (the "|"
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; yes, there is one good idea
-in PHP).
+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
diff --git a/docs/intro/reusable-apps.txt b/docs/intro/reusable-apps.txt
new file mode 100644
index 0000000000..6aade4997e
--- /dev/null
+++ b/docs/intro/reusable-apps.txt
@@ -0,0 +1,365 @@
+=============================================
+Advanced tutorial: How to write reusable apps
+=============================================
+
+This advanced tutorial begins where :doc:`Tutorial 5 </intro/tutorial05>` left
+off. We'll be turning our Web-poll into a standalone Python package you can
+reuse in new projects and share with other people.
+
+If you haven't recently completed Tutorials 1–5, we encourage you to review
+these so that your example project matches the one described below.
+
+Reusability matters
+===================
+
+It's a lot of work to design, build, test and maintain a web application. Many
+Python and Django projects share common problems. Wouldn't it be great if we
+could save some of this repeated work?
+
+Reusability is the way of life in Python. `The Python Package Index (PyPI)
+<http://guide.python-distribute.org/contributing.html#pypi-info>`_ has a vast
+range of packages you can use in your own Python programs. Check out `Django
+Packages <http://www.djangopackages.com>`_ for existing reusable apps you could
+incorporate in your project. Django itself is also just a Python package. This
+means that you can take existing Python packages or Django apps and compose
+them into your own web project. You only need to write the parts that make
+your project unique.
+
+Let's say you were starting a new project that needed a polls app like the one
+we've been working on. How do you make this app reusable? Luckily, you're well
+on the way already. In :doc:`Tutorial 3 </intro/tutorial03>`, we saw how we
+could decouple polls from the project-level URLconf using an ``include``.
+In this tutorial, we'll take further steps to make the app easy to use in new
+projects and ready to publish for others to install and use.
+
+.. admonition:: Package? App?
+
+ A Python `package <http://docs.python.org/tutorial/modules.html#packages>`_
+ provides a way of grouping related Python code for easy reuse. A package
+ contains one or more files of Python code (also known as "modules").
+
+ A package can be imported with ``import foo.bar`` or ``from foo import
+ bar``. For a directory (like ``polls``) to form a package, it must contain
+ a special file ``__init__.py``, even if this file is empty.
+
+ A Django *app* is just a Python package that is specifically intended for
+ use in a Django project. An app may also use common Django conventions,
+ such as having a ``models.py`` file.
+
+ Later on we use the term *packaging* to describe the process of making a
+ Python package easy for others to install. It can be a little confusing, we
+ know.
+
+Completing your reusable app
+============================
+
+After the previous tutorials, our project should look like this::
+
+ mysite/
+ manage.py
+ mysite/
+ __init__.py
+ settings.py
+ urls.py
+ wsgi.py
+ polls/
+ admin.py
+ __init__.py
+ models.py
+ tests.py
+ urls.py
+ views.py
+
+You also have a directory somewhere called ``mytemplates`` which you created in
+:doc:`Tutorial 2 </intro/tutorial02>`. You specified its location in the
+TEMPLATE_DIRS setting. This directory should look like this::
+
+ mytemplates/
+ admin/
+ base_site.html
+ polls/
+ detail.html
+ index.html
+ results.html
+
+The polls app is already a Python package, thanks to the ``polls/__init__.py``
+file. That's a great start, but we can't just pick up this package and drop it
+into a new project. The polls templates are currently stored in the
+project-wide ``mytemplates`` directory. To make the app self-contained, it
+should also contain the necessary templates.
+
+Inside the ``polls`` app, create a new ``templates`` directory. Now move the
+``polls`` template directory from ``mytemplates`` into the new
+``templates``. Your project should now look like this::
+
+ mysite/
+ manage.py
+ mysite/
+ __init__.py
+ settings.py
+ urls.py
+ wsgi.py
+ polls/
+ admin.py
+ __init__.py
+ models.py
+ templates/
+ polls/
+ detail.html
+ index.html
+ results.html
+ tests.py
+ urls.py
+ views.py
+
+Your project-wide templates directory should now look like this::
+
+ mytemplates/
+ admin/
+ base_site.html
+
+Looking good! Now would be a good time to confirm that your polls application
+still works correctly. How does Django know how to find the new location of
+the polls templates even though we didn't modify :setting:`TEMPLATE_DIRS`?
+Django has a :setting:`TEMPLATE_LOADERS` setting which contains a list
+of callables that know how to import templates from various sources. One of
+the defaults is :class:`django.template.loaders.app_directories.Loader` which
+looks for a "templates" subdirectory in each of the :setting:`INSTALLED_APPS`.
+
+The ``polls`` directory could now be copied into a new Django project and
+immediately reused. It's not quite ready to be published though. For that, we
+need to package the app to make it easy for others to install.
+
+.. admonition:: Why nested?
+
+ Why create a ``polls`` directory under ``templates`` when we're
+ already inside the polls app? This directory is needed to avoid conflicts in
+ Django's ``app_directories`` template loader. For example, if two
+ apps had a template called ``base.html``, without the extra directory it
+ wouldn't be possible to distinguish between the two. It's a good convention
+ to use the name of your app for this directory.
+
+.. _installing-reusable-apps-prerequisites:
+
+Installing some prerequisites
+=============================
+
+The current state of Python packaging is a bit muddled with various tools. For
+this tutorial, we're going to use distribute_ to build our package. It's a
+community-maintained fork of the older ``setuptools`` project. We'll also be
+using `pip`_ to uninstall it after we're finished. You should install these
+two packages now. If you need help, you can refer to :ref:`how to install
+Django with pip<installing-official-release>`. You can install ``distribute``
+the same way.
+
+.. _distribute: http://pypi.python.org/pypi/distribute
+.. _pip: http://pypi.python.org/pypi/pip
+
+Packaging your app
+==================
+
+Python *packaging* refers to preparing your app in a specific format that can
+be easily installed and used. Django itself is packaged very much like
+this. For a small app like polls, this process isn't too difficult.
+
+1. First, create a parent directory for ``polls``, outside of your Django
+ project. Call this directory ``django-polls``.
+
+.. admonition:: Choosing a name for your app
+
+ When choosing a name for your package, check resources like PyPI to avoid
+ naming conflicts with existing packages. It's often useful to prepend
+ ``django-`` to your module name when creating a package to distribute.
+ This helps others looking for Django apps identify your app as Django
+ specific.
+
+2. Move the ``polls`` directory into the ``django-polls`` directory.
+
+3. Create a file ``django-polls/README.txt`` with the following contents::
+
+ =====
+ Polls
+ =====
+
+ Polls is a simple Django app to conduct Web-based polls. For each
+ question, visitors can choose between a fixed number of answers.
+
+ Detailed documentation is in the "docs" directory.
+
+ Quick start
+ -----------
+
+ 1. Add "polls" to your INSTALLED_APPS setting like this::
+
+ INSTALLED_APPS = (
+ ...
+ 'polls',
+ )
+
+ 2. Include the polls URLconf in your project urls.py like this::
+
+ url(r'^polls/', include('polls.urls')),
+
+ 3. Run `python manage.py syncdb` to create the polls models.
+
+ 4. Start the development server and visit http://127.0.0.1:8000/admin/
+ to create a poll (you'll need the Admin app enabled).
+
+ 5. Visit http://127.0.0.1:8000/polls/ to participate in the poll.
+
+4. Create a ``django-polls/LICENSE`` file. Choosing a license is beyond the
+scope of this tutorial, but suffice it to say that code released publicly
+without a license is *useless*. Django and many Django-compatible apps are
+distributed under the BSD license; however, you're free to pick your own
+license. Just be aware that your licensing choice will affect who is able
+to use your code.
+
+5. Next we'll create a ``setup.py`` file which provides details about how to
+build and install the app. A full explanation of this file is beyond the
+scope of this tutorial, but the `distribute docs
+<http://packages.python.org/distribute/setuptools.html>`_ have a good explanation.
+Create a file ``django-polls/setup.py`` with the following contents::
+
+ import os
+ from setuptools import setup
+
+ README = open(os.path.join(os.path.dirname(__file__), 'README.txt')).read()
+
+ # allow setup.py to be run from any path
+ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
+
+ setup(
+ name = 'django-polls',
+ version = '0.1',
+ packages = ['polls'],
+ include_package_data = True,
+ license = 'BSD License', # example license
+ description = 'A simple Django app to conduct Web-based polls.',
+ long_description = README,
+ url = 'http://www.example.com/',
+ author = 'Your Name',
+ author_email = 'yourname@example.com',
+ classifiers = [
+ 'Environment :: Web Environment',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: BSD License', # example license
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
+ ],
+ )
+
+.. admonition:: I thought you said we were going to use ``distribute``?
+
+ Distribute is a drop-in replacement for ``setuptools``. Even though we
+ appear to import from ``setuptools``, since we have ``distribute``
+ installed, it will override the import.
+
+6. Only Python modules and packages are included in the package by default. To
+ include additional files, we'll need to create a ``MANIFEST.in`` file. The
+ distribute docs referred to in the previous step discuss this file in more
+ details. To include the templates and our LICENSE file, create a file
+ ``django-polls/MANIFEST.in`` with the following contents::
+
+ include LICENSE
+ recursive-include polls/templates *
+
+7. It's optional, but recommended, to include detailed documentation with your
+ app. Create an empty directory ``django-polls/docs`` for future
+ documentation. Add an additional line to ``django-polls/MANIFEST.in``::
+
+ recursive-include docs *
+
+ Note that the ``docs`` directory won't be included in your package unless
+ you add some files to it. Many Django apps also provide their documentation
+ online through sites like `readthedocs.org <http://readthedocs.org>`_.
+
+8. Try building your package with ``python setup.py sdist`` (run from inside
+ ``django-polls``). This creates a directory called ``dist`` and builds your
+ new package, ``django-polls-0.1.tar.gz``.
+
+For more information on packaging, see `The Hitchhiker's Guide to Packaging
+<http://guide.python-distribute.org/quickstart.html>`_.
+
+Using your own package
+======================
+
+Since we moved the ``polls`` directory out of the project, it's no longer
+working. We'll now fix this by installing our new ``django-polls`` package.
+
+.. admonition:: Installing as a user library
+
+ The following steps install ``django-polls`` as a user library. Per-user
+ installs have a lot of advantages over installing the package system-wide,
+ such as being usable on systems where you don't have administrator access
+ as well as preventing the package from affecting system services and other
+ users of the machine. Python 2.6 added support for user libraries, so if
+ you are using an older version this won't work, but Django 1.5 requires
+ Python 2.6 or newer anyway.
+
+ Note that per-user installations can still affect the behavior of system
+ tools that run as that user, so ``virtualenv`` is a more robust solution
+ (see below).
+
+1. Inside ``django-polls/dist``, untar the new package
+ ``django-polls-0.1.tar.gz`` (e.g. ``tar xzvf django-polls-0.1.tar.gz``). If
+ you're using Windows, you can download the command-line tool bsdtar_ to do
+ this, or you can use a GUI-based tool such as 7-zip_.
+
+2. Change into the directory created in step 1 (e.g. ``cd django-polls-0.1``).
+
+3. If you're using GNU/Linux, Mac OS X or some other flavor of Unix, enter the
+ command ``python setup.py install --user`` at the shell prompt. If you're
+ using Windows, start up a command shell and run the command
+ ``setup.py install --user``.
+
+ With luck, your Django project should now work correctly again. Run the
+ server again to confirm this.
+
+4. To uninstall the package, use pip (you already :ref:`installed it
+ <installing-reusable-apps-prerequisites>`, right?)::
+
+ pip uninstall django-polls
+
+.. _bsdtar: http://gnuwin32.sourceforge.net/packages/bsdtar.htm
+.. _7-zip: http://www.7-zip.org/
+.. _pip: http://pypi.python.org/pypi/pip
+
+Publishing your app
+===================
+
+Now that we've packaged and tested ``django-polls``, it's ready to share with
+the world! If this wasn't just an example, you could now:
+
+* Email the package to a friend.
+
+* Upload the package on your Web site.
+
+* Post the package on a public repository, such as `The Python Package Index
+ (PyPI) <http://guide.python-distribute.org/contributing.html#pypi-info>`_.
+
+For more information on PyPI, see the `Quickstart
+<http://guide.python-distribute.org/quickstart.html#register-your-package-with-the-python-package-index-pypi>`_
+section of The Hitchhiker's Guide to Packaging. One detail this guide mentions
+is choosing the license under which your code is distributed.
+
+Installing Python packages with virtualenv
+==========================================
+
+Earlier, we installed the polls app as a user library. This has some
+disadvantages:
+
+* Modifying the user libraries can affect other Python software on your system.
+
+* You won't be able to run multiple versions of this package (or others with
+ the same name).
+
+Typically, these situations only arise once you're maintaining several Django
+projects. When they do, the best solution is to use `virtualenv
+<http://www.virtualenv.org/>`_. This tool allows you to maintain multiple
+isolated Python environments, each with its own copy of the libraries and
+package namespace.
diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt
index 1e2231d1e0..9419f9c4eb 100644
--- a/docs/intro/tutorial01.txt
+++ b/docs/intro/tutorial01.txt
@@ -52,25 +52,8 @@ code, then run the following command:
django-admin.py startproject mysite
-This will create a ``mysite`` directory in your current directory. If it didn't
-work, see :doc:`Troubleshooting </faq/troubleshooting>`.
-
-.. admonition:: Script name may differ in distribution packages
-
- If you installed Django using a Linux distribution's package manager
- (e.g. apt-get or yum) ``django-admin.py`` may have been renamed to
- ``django-admin``. You may continue through this documentation by omitting
- ``.py`` from each command.
-
-.. admonition:: Mac OS X permissions
-
- If you're using Mac OS X, you may see the message "permission denied" when
- you try to run ``django-admin.py startproject``. This is because, on
- Unix-based systems like OS X, a file must be marked as "executable" before it
- can be run as a program. To do this, open Terminal.app and navigate (using
- the ``cd`` command) to the directory where :doc:`django-admin.py
- </ref/django-admin>` is installed, then run the command
- ``sudo chmod +x django-admin.py``.
+This will create a ``mysite`` directory in your current directory. If it didn't
+work, see :ref:`troubleshooting-django-admin-py`.
.. note::
@@ -81,12 +64,12 @@ work, see :doc:`Troubleshooting </faq/troubleshooting>`.
.. admonition:: Where should this code live?
- If your background is in PHP, you're probably used to putting code under the
- Web server's document root (in a place such as ``/var/www``). With Django,
- you don't do that. It's not a good idea to put any of this Python code
- within your Web server's document root, because it risks the possibility
- that people may be able to view your code over the Web. That's not good for
- security.
+ If your background is in plain old PHP (with no use of modern frameworks),
+ you're probably used to putting code under the Web server's document root
+ (in a place such as ``/var/www``). With Django, you don't do that. It's
+ not a good idea to put any of this Python code within your Web server's
+ document root, because it risks the possibility that people may be able
+ to view your code over the Web. That's not good for security.
Put your code in some directory **outside** of the document root, such as
:file:`/home/mycode`.
@@ -220,8 +203,8 @@ your database connection settings.
SQLite).
* :setting:`HOST` -- The host your database is on. Leave this as
- an empty string if your database server is on the same physical
- machine (not used for SQLite).
+ an empty string (or possibly ``127.0.0.1``) if your database server is on the
+ same physical machine (not used for SQLite). See :setting:`HOST` for details.
If you're new to databases, we recommend simply using SQLite by setting
:setting:`ENGINE` to ``'django.db.backends.sqlite3'`` and :setting:`NAME` to
@@ -666,6 +649,7 @@ Save these changes and start a new Python interactive shell by running
>>> Poll.objects.get(pub_date__year=2012)
<Poll: What's up?>
+ # Request an ID that doesn't exist, this will raise an exception.
>>> Poll.objects.get(id=2)
Traceback (most recent call last):
...
diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt
index b87b280d7c..2c8d25ae6f 100644
--- a/docs/intro/tutorial02.txt
+++ b/docs/intro/tutorial02.txt
@@ -445,11 +445,6 @@ live anywhere on your filesystem that Django can access. (Django runs as
whatever user your server runs.) However, keeping your templates within the
project is a good convention to follow.
-When you’ve done that, create a directory polls in your template directory.
-Within that, create a file called index.html. Note that our
-``loader.get_template('polls/index.html')`` code from above maps to
-[template_directory]/polls/index.html” on the filesystem.
-
By default, :setting:`TEMPLATE_DIRS` is empty. So, let's add a line to it, to
tell Django where our templates live::
diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt
index 169e6cd59f..3159ee88c2 100644
--- a/docs/intro/tutorial03.txt
+++ b/docs/intro/tutorial03.txt
@@ -235,11 +235,11 @@ be used to identify the matched pattern; and ``\d+`` is a regular expression to
match a sequence of digits (i.e., a number).
Because the URL patterns are regular expressions, there really is no limit on
-what you can do with them. And there's no need to add URL cruft such as ``.php``
--- unless you have a sick sense of humor, in which case you can do something
-like this::
+what you can do with them. And there's no need to add URL cruft such as
+``.html`` -- unless you want to, in which case you can do something like
+this::
- (r'^polls/latest\.php$', 'polls.views.index'),
+ (r'^polls/latest\.html$', 'polls.views.index'),
But, don't do that. It's silly.
@@ -315,6 +315,12 @@ Load the page in your Web browser, and you should see a bulleted-list
containing the "What's up" poll from Tutorial 1. The link points to the poll's
detail page.
+.. admonition:: Organizing Templates
+
+ Rather than one big templates directory, you can also store templates
+ within each app. We'll discuss this in more detail in the :doc:`reusable
+ apps tutorial</intro/reusable-apps>`.
+
A shortcut: :func:`~django.shortcuts.render`
--------------------------------------------
diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt
index 8909caf98b..1619b599bb 100644
--- a/docs/intro/tutorial04.txt
+++ b/docs/intro/tutorial04.txt
@@ -275,8 +275,5 @@ Run the server, and use your new polling app based on generic views.
For full details on generic views, see the :doc:`generic views documentation
</topics/class-based-views/index>`.
-What's next?
-============
-
-The tutorial ends here for the time being. In the meantime, you might want to
-check out some pointers on :doc:`where to go from here </intro/whatsnext>`.
+When you're comfortable with forms and generic views, read :doc:`part 5 of this
+tutorial</intro/tutorial05>` to learn about testing our polls app.
diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt
new file mode 100644
index 0000000000..163b7cdd0f
--- /dev/null
+++ b/docs/intro/tutorial05.txt
@@ -0,0 +1,650 @@
+=====================================
+Writing your first Django app, part 5
+=====================================
+
+This tutorial begins where :doc:`Tutorial 4 </intro/tutorial04>` left off.
+We've built a Web-poll application, and we'll now create some automated tests
+for it.
+
+Introducing automated testing
+=============================
+
+What are automated tests?
+-------------------------
+
+Tests are simple routines that check the operation of your code.
+
+Testing operates at different levels. Some tests might apply to a tiny detail
+- *does a particular model method return values as expected?*, while others
+examine the overall operation of the software - *does a sequence of user inputs
+on the site produce the desired result?* That's no different from the kind of
+testing you did earlier in :doc:`Tutorial 1 </intro/tutorial01>`, using the
+shell to examine the behavior of a method, or running the application and
+entering data to check how it behaves.
+
+What's different in *automated* tests is that the testing work is done for
+you by the system. You create a set of tests once, and then as you make changes
+to your app, you can check that your code still works as you originally
+intended, without having to perform time consuming manual testing.
+
+Why you need to create tests
+----------------------------
+
+So why create tests, and why now?
+
+You may feel that you have quite enough on your plate just learning
+Python/Django, and having yet another thing to learn and do may seem
+overwhelming and perhaps unnecessary. After all, our polls application is
+working quite happily now; going through the trouble of creating automated
+tests is not going to make it work any better. If creating the polls
+application is the last bit of Django programming you will ever do, then true,
+you don't need to know how to create automated tests. But, if that's not the
+case, now is an excellent time to learn.
+
+Tests will save you time
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Up to a certain point, 'checking that it seems to work' will be a satisfactory
+test. In a more sophisticated application, you might have dozens of complex
+interactions between components.
+
+A change in any of those components could have unexpected consequences on the
+application's behavior. Checking that it still 'seems to work' could mean
+running through your code's functionality with twenty different variations of
+your test data just to make sure you haven't broken something - not a good use
+of your time.
+
+That's especially true when automated tests could do this for you in seconds.
+If something's gone wrong, tests will also assist in identifying the code
+that's causing the unexpected behavior.
+
+Sometimes it may seem a chore to tear yourself away from your productive,
+creative programming work to face the unglamorous and unexciting business
+of writing tests, particularly when you know your code is working properly.
+
+However, the task of writing tests is a lot more fulfilling than spending hours
+testing your application manually or trying to identify the cause of a
+newly-introduced problem.
+
+Tests don't just identify problems, they prevent them
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+It's a mistake to think of tests merely as a negative aspect of development.
+
+Without tests, the purpose or intended behavior of an application might be
+rather opaque. Even when it's your own code, you will sometimes find yourself
+poking around in it trying to find out what exactly it's doing.
+
+Tests change that; they light up your code from the inside, and when something
+goes wrong, they focus light on the part that has gone wrong - *even if you
+hadn't even realized it had gone wrong*.
+
+Tests make your code more attractive
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You might have created a brilliant piece of software, but you will find that
+many other developers will simply refuse to look at it because it lacks tests;
+without tests, they won't trust it. Jacob Kaplan-Moss, one of Django's
+original developers, says "Code without tests is broken by design."
+
+That other developers want to see tests in your software before they take it
+seriously is yet another reason for you to start writing tests.
+
+Tests help teams work together
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The previous points are written from the point of view of a single developer
+maintaining an application. Complex applications will be maintained by teams.
+Tests guarantee that colleagues don't inadvertently break your code (and that
+you don't break theirs without knowing). If you want to make a living as a
+Django programmer, you must be good at writing tests!
+
+Basic testing strategies
+========================
+
+There are many ways to approach writing tests.
+
+Some programmers follow a discipline called "`test-driven development`_"; they
+actually write their tests before they write their code. This might seem
+counter-intuitive, but in fact it's similar to what most people will often do
+anyway: they describe a problem, then create some code to solve it. Test-driven
+development simply formalizes the problem in a Python test case.
+
+More often, a newcomer to testing will create some code and later decide that
+it should have some tests. Perhaps it would have been better to write some
+tests earlier, but it's never too late to get started.
+
+Sometimes it's difficult to figure out where to get started with writing tests.
+If you have written several thousand lines of Python, choosing something to
+test might not be easy. In such a case, it's fruitful to write your first test
+the next time you make a change, either when you add a new feature or fix a bug.
+
+So let's do that right away.
+
+.. _test-driven development: http://en.wikipedia.org/wiki/Test-driven_development/
+
+Writing our first test
+======================
+
+We identify a bug
+-----------------
+
+Fortunately, there's a little bug in the ``polls`` application for us to fix
+right away: the ``Poll.was_published_recently()`` method returns ``True`` if
+the ``Poll`` was published within the last day (which is correct) but also if
+the ``Poll``'s ``pub_date`` field is in the future (which certainly isn't).
+
+You can see this in the Admin; create a poll whose date lies in the future;
+you'll see that the ``Poll`` change list claims it was published recently.
+
+You can also see this using the shell::
+
+ >>> import datetime
+ >>> from django.utils import timezone
+ >>> from polls.models import Poll
+ >>> # create a Poll instance with pub_date 30 days in the future
+ >>> future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
+ >>> # was it published recently?
+ >>> future_poll.was_published_recently()
+ True
+
+Since things in the future are not 'recent', this is clearly wrong.
+
+Create a test to expose the bug
+-------------------------------
+
+What we've just done in the shell to test for the problem is exactly what we
+can do in an automated test, so let's turn that into an automated test.
+
+The best place for an application's tests is in the application's ``tests.py``
+file - the testing system will look there for tests automatically.
+
+Put the following in the ``tests.py`` file in the ``polls`` application (you'll
+notice ``tests.py`` contains some dummy tests, you can remove those)::
+
+ import datetime
+
+ from django.utils import timezone
+ from django.test import TestCase
+
+ from polls.models import Poll
+
+ class PollMethodTests(TestCase):
+
+ def test_was_published_recently_with_future_poll(self):
+ """
+ was_published_recently() should return False for polls whose
+ pub_date is in the future
+ """
+ future_poll = Poll(pub_date=timezone.now() + datetime.timedelta(days=30))
+ self.assertEqual(future_poll.was_published_recently(), False)
+
+What we have done here is created a :class:`django.test.TestCase` subclass
+with a method that creates a ``Poll`` instance with a ``pub_date`` in the
+future. We then check the output of ``was_published_recently()`` - which
+*ought* to be False.
+
+Running tests
+-------------
+
+In the terminal, we can run our test::
+
+ python manage.py test polls
+
+and you'll see something like::
+
+ Creating test database for alias 'default'...
+ F
+ ======================================================================
+ FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
+ ----------------------------------------------------------------------
+ Traceback (most recent call last):
+ File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
+ self.assertEqual(future_poll.was_published_recently(), False)
+ AssertionError: True != False
+
+ ----------------------------------------------------------------------
+ Ran 1 test in 0.001s
+
+ FAILED (failures=1)
+ Destroying test database for alias 'default'...
+
+What happened is this:
+
+* ``python manage.py test polls`` looked for tests in the ``polls`` application
+
+* it found a subclass of the :class:`django.test.TestCase` class
+
+* it created a special database for the purpose of testing
+
+* it looked for test methods - ones whose names begin with ``test``
+
+* in ``test_was_published_recently_with_future_poll`` it created a ``Poll``
+ instance whose ``pub_date`` field is 30 days in the future
+
+* ... and using the ``assertEqual()`` method, it discovered that its
+ ``was_published_recently()`` returns ``True``, though we wanted it to return
+ ``False``
+
+The test informs us which test failed and even the line on which the failure
+occurred.
+
+Fixing the bug
+--------------
+
+We already know what the problem is: ``Poll.was_published_recently()`` should
+return ``False`` if its ``pub_date`` is in the future. Amend the method in
+``models.py``, so that it will only return ``True`` if the date is also in the
+past::
+
+ def was_published_recently(self):
+ now = timezone.now()
+ return now - datetime.timedelta(days=1) <= self.pub_date < now
+
+and run the test again::
+
+ Creating test database for alias 'default'...
+ .
+ ----------------------------------------------------------------------
+ Ran 1 test in 0.001s
+
+ OK
+ Destroying test database for alias 'default'...
+
+After identifying a bug, we wrote a test that exposes it and corrected the bug
+in the code so our test passes.
+
+Many other things might go wrong with our application in the future, but we can
+be sure that we won't inadvertently reintroduce this bug, because simply
+running the test will warn us immediately. We can consider this little portion
+of the application pinned down safely forever.
+
+More comprehensive tests
+------------------------
+
+While we're here, we can further pin down the ``was_published_recently()``
+method; in fact, it would be positively embarrassing if in fixing one bug we had
+introduced another.
+
+Add two more test methods to the same class, to test the behavior of the method
+more comprehensively::
+
+ def test_was_published_recently_with_old_poll(self):
+ """
+ was_published_recently() should return False for polls whose pub_date
+ is older than 1 day
+ """
+ old_poll = Poll(pub_date=timezone.now() - datetime.timedelta(days=30))
+ self.assertEqual(old_poll.was_published_recently(), False)
+
+ def test_was_published_recently_with_recent_poll(self):
+ """
+ was_published_recently() should return True for polls whose pub_date
+ is within the last day
+ """
+ recent_poll = Poll(pub_date=timezone.now() - datetime.timedelta(hours=1))
+ self.assertEqual(recent_poll.was_published_recently(), True)
+
+And now we have three tests that confirm that ``Poll.was_published_recently()``
+returns sensible values for past, recent, and future polls.
+
+Again, ``polls`` is a simple application, but however complex it grows in the
+future and whatever other code it interacts with, we now have some guarantee
+that the method we have written tests for will behave in expected ways.
+
+Test a view
+===========
+
+The polls application is fairly undiscriminating: it will publish any poll,
+including ones whose ``pub_date`` field lies in the future. We should improve
+this. Setting a ``pub_date`` in the future should mean that the Poll is
+published at that moment, but invisible until then.
+
+A test for a view
+-----------------
+
+When we fixed the bug above, we wrote the test first and then the code to fix
+it. In fact that was a simple example of test-driven development, but it
+doesn't really matter in which order we do the work.
+
+In our first test, we focused closely on the internal behavior of the code. For
+this test, we want to check its behavior as it would be experienced by a user
+through a web browser.
+
+Before we try to fix anything, let's have a look at the tools at our disposal.
+
+The Django test client
+----------------------
+
+Django provides a test :class:`~django.test.client.Client` to simulate a user
+interacting with the code at the view level. We can use it in ``tests.py``
+or even in the shell.
+
+We will start again with the shell, where we need to do a couple of things that
+won't be necessary in ``tests.py``. The first is to set up the test environment
+in the shell::
+
+ >>> from django.test.utils import setup_test_environment
+ >>> setup_test_environment()
+
+Next we need to import the test client class (later in ``tests.py`` we will use
+the :class:`django.test.TestCase` class, which comes with its own client, so
+this won't be required)::
+
+ >>> from django.test.client import Client
+ >>> # create an instance of the client for our use
+ >>> client = Client()
+
+With that ready, we can ask the client to do some work for us::
+
+ >>> # get a response from '/'
+ >>> response = client.get('/')
+ >>> # we should expect a 404 from that address
+ >>> response.status_code
+ 404
+ >>> # on the other hand we should expect to find something at '/polls/'
+ >>> # we'll use 'reverse()' rather than a harcoded URL
+ >>> from django.core.urlresolvers import reverse
+ >>> response = client.get(reverse('polls:index'))
+ >>> response.status_code
+ 200
+ >>> response.content
+ '\n\n\n <p>No polls are available.</p>\n\n'
+ >>> # note - you might get unexpected results if your ``TIME_ZONE``
+ >>> # in ``settings.py`` is not correct. If you need to change it,
+ >>> # you will also need to restart your shell session
+ >>> from polls.models import Poll
+ >>> from django.utils import timezone
+ >>> # create a Poll and save it
+ >>> p = Poll(question="Who is your favorite Beatle?", pub_date=timezone.now())
+ >>> p.save()
+ >>> # check the response once again
+ >>> response = client.get('/polls/')
+ >>> response.content
+ '\n\n\n <ul>\n \n <li><a href="/polls/1/">Who is your favorite Beatle?</a></li>\n \n </ul>\n\n'
+ >>> response.context['latest_poll_list']
+ [<Poll: Who is your favorite Beatle?>]
+
+Improving our view
+------------------
+
+The list of polls shows polls that aren't published yet (i.e. those that have a
+``pub_date`` in the future). Let's fix that.
+
+In :doc:`Tutorial 4 </intro/tutorial04>` we deleted the view functions from
+``views.py`` in favor of a :class:`~django.views.generic.list.ListView` in
+``urls.py``::
+
+ url(r'^$',
+ ListView.as_view(
+ queryset=Poll.objects.order_by('-pub_date')[:5],
+ context_object_name='latest_poll_list',
+ template_name='polls/index.html'),
+ name='index'),
+
+``response.context_data['latest_poll_list']`` extracts the data this view
+places into the context.
+
+We need to amend the line that gives us the ``queryset``::
+
+ queryset=Poll.objects.order_by('-pub_date')[:5],
+
+Let's change the queryset so that it also checks the date by comparing it with
+``timezone.now()``. First we need to add an import::
+
+ from django.utils import timezone
+
+and then we must amend the existing ``url`` function to::
+
+ url(r'^$',
+ ListView.as_view(
+ queryset=Poll.objects.filter(pub_date__lte=timezone.now) \
+ .order_by('-pub_date')[:5],
+ context_object_name='latest_poll_list',
+ template_name='polls/index.html'),
+ name='index'),
+
+``Poll.objects.filter(pub_date__lte=timezone.now)`` returns a queryset
+containing Polls whose ``pub_date`` is less than or equal to - that is, earlier
+than or equal to - ``timezone.now``. Notice that we use a callable queryset
+argument, ``timezone.now``, which will be evaluated at request time. If we had
+included the parentheses, ``timezone.now()`` would be evaluated just once when
+the web server is started.
+
+Testing our new view
+--------------------
+
+Now you can satisfy yourself that this behaves as expected by firing up the
+runserver, loading the site in your browser, creating ``Polls`` with dates in
+the past and future, and checking that only those that have been published are
+listed. You don't want to have to do that *every single time you make any
+change that might affect this* - so let's also create a test, based on our
+shell session above.
+
+Add the following to ``polls/tests.py``::
+
+ from django.core.urlresolvers import reverse
+
+and we'll create a factory method to create polls as well as a new test class::
+
+ def create_poll(question, days):
+ """
+ Creates a poll with the given `question` published the given number of
+ `days` offset to now (negative for polls published in the past,
+ positive for polls that have yet to be published).
+ """
+ return Poll.objects.create(question=question,
+ pub_date=timezone.now() + datetime.timedelta(days=days))
+
+ class PollViewTests(TestCase):
+ def test_index_view_with_no_polls(self):
+ """
+ If no polls exist, an appropriate message should be displayed.
+ """
+ response = self.client.get(reverse('polls:index'))
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "No polls are available.")
+ self.assertQuerysetEqual(response.context['latest_poll_list'], [])
+
+ def test_index_view_with_a_past_poll(self):
+ """
+ Polls with a pub_date in the past should be displayed on the index page.
+ """
+ create_poll(question="Past poll.", days=-30)
+ response = self.client.get(reverse('polls:index'))
+ self.assertQuerysetEqual(
+ response.context['latest_poll_list'],
+ ['<Poll: Past poll.>']
+ )
+
+ def test_index_view_with_a_future_poll(self):
+ """
+ Polls with a pub_date in the future should not be displayed on the
+ index page.
+ """
+ create_poll(question="Future poll.", days=30)
+ response = self.client.get(reverse('polls:index'))
+ self.assertContains(response, "No polls are available.", status_code=200)
+ self.assertQuerysetEqual(response.context['latest_poll_list'], [])
+
+ def test_index_view_with_future_poll_and_past_poll(self):
+ """
+ Even if both past and future polls exist, only past polls should be
+ displayed.
+ """
+ create_poll(question="Past poll.", days=-30)
+ create_poll(question="Future poll.", days=30)
+ response = self.client.get(reverse('polls:index'))
+ self.assertQuerysetEqual(
+ response.context['latest_poll_list'],
+ ['<Poll: Past poll.>']
+ )
+
+ def test_index_view_with_two_past_polls(self):
+ """
+ The polls index page may display multiple polls.
+ """
+ create_poll(question="Past poll 1.", days=-30)
+ create_poll(question="Past poll 2.", days=-5)
+ response = self.client.get(reverse('polls:index'))
+ self.assertQuerysetEqual(
+ response.context['latest_poll_list'],
+ ['<Poll: Past poll 2.>', '<Poll: Past poll 1.>']
+ )
+
+Let's look at some of these more closely.
+
+First is a poll factory method, ``create_poll``, to take some repetition out
+of the process of creating polls.
+
+``test_index_view_with_no_polls`` doesn't create any polls, but checks the
+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()`.
+
+In ``test_index_view_with_a_past_poll``, we create a poll and verify that it
+appears in the list.
+
+In ``test_index_view_with_a_future_poll``, we create a poll with a ``pub_date``
+in the future. The database is reset for each test method, so the first poll is
+no longer there, and so again the index shouldn't have any polls in it.
+
+And so on. In effect, we are using the tests to tell a story of admin input
+and user experience on the site, and checking that at every state and for every
+new change in the state of the system, the expected results are published.
+
+Testing the ``DetailView``
+--------------------------
+
+What we have works well; however, even though future polls don't appear in the
+*index*, users can still reach them if they know or guess the right URL. So we
+need similar constraints in the ``DetailViews``, by adding::
+
+ queryset=Poll.objects.filter(pub_date__lte=timezone.now)
+
+to them - for example::
+
+ url(r'^(?P<pk>\d+)/$',
+ DetailView.as_view(
+ queryset=Poll.objects.filter(pub_date__lte=timezone.now),
+ model=Poll,
+ template_name='polls/detail.html'),
+ name='detail'),
+
+and of course, we will add some tests, to check that a ``Poll`` whose
+``pub_date`` is in the past can be displayed, and that one with a ``pub_date``
+in the future is not::
+
+ class PollIndexDetailTests(TestCase):
+ def test_detail_view_with_a_future_poll(self):
+ """
+ The detail view of a poll with a pub_date in the future should
+ return a 404 not found.
+ """
+ future_poll = create_poll(question='Future poll.', days=5)
+ response = self.client.get(reverse('polls:detail', args=(future_poll.id,)))
+ self.assertEqual(response.status_code, 404)
+
+ def test_detail_view_with_a_past_poll(self):
+ """
+ The detail view of a poll with a pub_date in the past should display
+ the poll's question.
+ """
+ past_poll = create_poll(question='Past Poll.', days=-5)
+ response = self.client.get(reverse('polls:detail', args=(past_poll.id,)))
+ self.assertContains(response, past_poll.question, status_code=200)
+
+Ideas for more tests
+--------------------
+
+We ought to add similar ``queryset`` arguments to the other ``DetailView``
+URLs, and create a new test class for each view. They'll be very similar to
+what we have just created; in fact there will be a lot of repetition.
+
+We could also improve our application in other ways, adding tests along the
+way. For example, it's silly that ``Polls`` can be published on the site that
+have no ``Choices``. So, our views could check for this, and exclude such
+``Polls``. Our tests would create a ``Poll`` without ``Choices`` and then test
+that it's not published, as well as create a similar ``Poll`` *with*
+``Choices``, and test that it *is* published.
+
+Perhaps logged-in admin users should be allowed to see unpublished ``Polls``,
+but not ordinary visitors. Again: whatever needs to be added to the software to
+accomplish this should be accompanied by a test, whether you write the test
+first and then make the code pass the test, or work out the logic in your code
+first and then write a test to prove it.
+
+At a certain point you are bound to look at your tests and wonder whether your
+code is suffering from test bloat, which brings us to:
+
+When testing, more is better
+============================
+
+It might seem that our tests are growing out of control. At this rate there will
+soon be more code in our tests than in our application, and the repetition
+is unaesthetic, compared to the elegant conciseness of the rest of our code.
+
+**It doesn't matter**. Let them grow. For the most part, you can write a test
+once and then forget about it. It will continue performing its useful function
+as you continue to develop your program.
+
+Sometimes tests will need to be updated. Suppose that we amend our views so that
+only ``Polls`` with ``Choices`` are published. In that case, many of our
+existing tests will fail - *telling us exactly which tests need to be amended to
+bring them up to date*, so to that extent tests help look after themselves.
+
+At worst, as you continue developing, you might find that you have some tests
+that are now redundant. Even that's not a problem; in testing redundancy is
+a *good* thing.
+
+As long as your tests are sensibly arranged, they won't become unmanageable.
+Good rules-of-thumb include having:
+
+* a separate ``TestClass`` for each model or view
+* a separate test method for each set of conditions you want to test
+* test method names that describe their function
+
+Further testing
+===============
+
+This tutorial only introduces some of the basics of testing. There's a great
+deal more you can do, and a number of very useful tools at your disposal to
+achieve some very clever things.
+
+For example, while our tests here have covered some of the internal logic of a
+model and the way our views publish information, you can use an "in-browser"
+framework such as Selenium_ to test the way your HTML actually renders in a
+browser. These tools allow you to check not just the behavior of your Django
+code, but also, for example, of your JavaScript. It's quite something to see
+the tests launch a browser, and start interacting with your site, as if a human
+being were driving it! Django includes :class:`~django.test.LiveServerTestCase`
+to facilitate integration with tools like Selenium.
+
+If you have a complex application, you may want to run tests automatically
+with every commit for the purposes of `continuous integration`_, so that
+quality control is itself - at least partially - automated.
+
+A good way to spot untested parts of your application is to check code
+coverage. This also helps identify fragile or even dead code. If you can't test
+a piece of code, it usually means that code should be refactored or removed.
+Coverage will help to identify dead code. See
+:ref:`topics-testing-code-coverage` for details.
+
+:doc:`Testing Django applications </topics/testing>` has comprehensive
+information about testing.
+
+.. _Selenium: http://seleniumhq.org/
+.. _continuous integration: http://en.wikipedia.org/wiki/Continuous_integration
+
+What's next?
+============
+
+The beginner tutorial ends here for the time being. In the meantime, you might
+want to check out some pointers on :doc:`where to go from here
+</intro/whatsnext>`.
+
+If you are familiar with Python packaging and interested in learning how to
+turn polls into a "reusable app", check out :doc:`Advanced tutorial: How to
+write reusable apps</intro/reusable-apps>`.
diff --git a/docs/make.bat b/docs/make.bat
index d6299521eb..d7f54b2059 100644
--- a/docs/make.bat
+++ b/docs/make.bat
@@ -29,6 +29,7 @@ if "%1" == "help" (
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
+ echo. texinfo to make a Texinfo source file
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
@@ -143,6 +144,14 @@ if "%1" == "man" (
goto end
)
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
if "%%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
diff --git a/docs/misc/design-philosophies.txt b/docs/misc/design-philosophies.txt
index c4fb582df9..0101da9586 100644
--- a/docs/misc/design-philosophies.txt
+++ b/docs/misc/design-philosophies.txt
@@ -203,9 +203,6 @@ We see a template system as a tool that controls presentation and
presentation-related logic -- and that's it. The template system shouldn't
support functionality that goes beyond this basic goal.
-If we wanted to put everything in templates, we'd be using PHP. Been there,
-done that, wised up.
-
Discourage redundancy
---------------------
diff --git a/docs/ref/class-based-views/flattened-index.txt b/docs/ref/class-based-views/flattened-index.txt
index cbce3690e2..aa2f51f156 100644
--- a/docs/ref/class-based-views/flattened-index.txt
+++ b/docs/ref/class-based-views/flattened-index.txt
@@ -116,6 +116,7 @@ ListView
* :attr:`~django.views.generic.base.View.http_method_names`
* :attr:`~django.views.generic.list.MultipleObjectMixin.model`
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`]
+* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class`
* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`]
* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class`
@@ -290,6 +291,7 @@ ArchiveIndexView
* :attr:`~django.views.generic.base.View.http_method_names`
* :attr:`~django.views.generic.list.MultipleObjectMixin.model`
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`]
+* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class`
* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`]
* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class`
@@ -325,6 +327,7 @@ YearArchiveView
* :attr:`~django.views.generic.dates.BaseYearArchiveView.make_object_list` [:meth:`~django.views.generic.dates.BaseYearArchiveView.get_make_object_list`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.model`
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`]
+* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class`
* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`]
* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class`
@@ -363,6 +366,7 @@ MonthArchiveView
* :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`]
* :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`]
+* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class`
* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`]
* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class`
@@ -401,6 +405,7 @@ WeekArchiveView
* :attr:`~django.views.generic.base.View.http_method_names`
* :attr:`~django.views.generic.list.MultipleObjectMixin.model`
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`]
+* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class`
* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`]
* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class`
@@ -443,6 +448,7 @@ DayArchiveView
* :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`]
* :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`]
+* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class`
* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`]
* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class`
@@ -487,6 +493,7 @@ TodayArchiveView
* :attr:`~django.views.generic.dates.MonthMixin.month` [:meth:`~django.views.generic.dates.MonthMixin.get_month`]
* :attr:`~django.views.generic.dates.MonthMixin.month_format` [:meth:`~django.views.generic.dates.MonthMixin.get_month_format`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`]
+* :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`]
* :attr:`~django.views.generic.list.MultipleObjectMixin.paginator_class`
* :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset`]
* :attr:`~django.views.generic.base.TemplateResponseMixin.response_class`
diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt
index c6af23e421..0ae0bcdf42 100644
--- a/docs/ref/class-based-views/generic-date-based.txt
+++ b/docs/ref/class-based-views/generic-date-based.txt
@@ -7,6 +7,21 @@ Generic date views
Date-based generic views, provided in :mod:`django.views.generic.dates`, are
views for displaying drilldown pages for date-based data.
+.. note::
+
+ Some of the examples on this page assume that an ``Article`` model has been
+ defined as follows in ``myapp/models.py``::
+
+ from django.db import models
+ from django.core.urlresolvers import reverse
+
+ class Article(models.Model):
+ title = models.CharField(max_length=200)
+ pub_date = models.DateField()
+
+ def get_absolute_url(self):
+ return reverse('article-detail', kwargs={'pk': self.pk})
+
ArchiveIndexView
----------------
@@ -35,6 +50,31 @@ ArchiveIndexView
month or day using the attribute ``date_list_period``. This also applies
to all subclass views.
+ **Example views.py**::
+
+ from django.conf.urls import patterns, url
+ from django.views.generic.dates import ArchiveIndexView
+
+ from myapp.models import Article
+
+ urlpatterns = patterns('',
+ url(r'^archive/$',
+ ArchiveIndexView.as_view(model=Article, date_field="pub_date"),
+ name="article_archive"),
+ )
+
+ **Example myapp/article_archive.html**:
+
+ .. code-block:: html+django
+
+ <ul>
+ {% for article in latest %}
+ <li>{{ article.pub_date }}: {{ article.title }}</li>
+ {% endfor %}
+ </ul>
+
+ This will output all articles.
+
YearArchiveView
---------------
@@ -109,6 +149,49 @@ YearArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_year``.
+ **Example views.py**::
+
+ from django.views.generic.dates import YearArchiveView
+
+ from myapp.models import Article
+
+ class ArticleYearArchiveView(YearArchiveView):
+ queryset = Article.objects.all()
+ date_field = "pub_date"
+ make_object_list = True
+ allow_future = True
+
+ **Example urls.py**::
+
+ from django.conf.urls import patterns, url
+
+ from myapp.views import ArticleYearArchiveView
+
+ urlpatterns = patterns('',
+ url(r'^(?P<year>\d{4})/$',
+ ArticleYearArchiveView.as_view(),
+ name="article_year_archive"),
+ )
+
+ **Example myapp/article_archive_year.html**:
+
+ .. code-block:: html+django
+
+ <ul>
+ {% for date in date_list %}
+ <li>{{ date|date }}</li>
+ {% endfor %}
+ </ul>
+
+ <div>
+ <h1>All Articles for {{ year|date:"Y" }}</h1>
+ {% for obj in object_list %}
+ <p>
+ {{ obj.title }} - {{ obj.pub_date|date:"F j, Y" }}
+ </p>
+ {% endfor %}
+ </div>
+
MonthArchiveView
----------------
@@ -162,6 +245,54 @@ MonthArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_month``.
+ **Example views.py**::
+
+ from django.views.generic.dates import MonthArchiveView
+
+ from myapp.models import Article
+
+ class ArticleMonthArchiveView(MonthArchiveView):
+ queryset = Article.objects.all()
+ date_field = "pub_date"
+ make_object_list = True
+ allow_future = True
+
+ **Example urls.py**::
+
+ from django.conf.urls import patterns, url
+
+ from myapp.views import ArticleMonthArchiveView
+
+ urlpatterns = patterns('',
+ # Example: /2012/aug/
+ url(r'^(?P<year>\d{4})/(?P<month>[-\w]+)/$',
+ ArticleMonthArchiveView.as_view(),
+ name="archive_month"),
+ # Example: /2012/08/
+ url(r'^(?P<year>\d{4})/(?P<month>\d+)/$',
+ ArticleMonthArchiveView.as_view(month_format='%m'),
+ name="archive_month_numeric"),
+ )
+
+ **Example myapp/article_archive_month.html**:
+
+ .. code-block:: html+django
+
+ <ul>
+ {% for article in object_list %}
+ <li>{{ article.pub_date|date:"F j, Y" }}: {{ article.title }}</li>
+ {% endfor %}
+ </ul>
+
+ <p>
+ {% if previous_month %}
+ Previous Month: {{ previous_month|date:"F Y" }}
+ {% endif %}
+ {% if next_month %}
+ Next Month: {{ next_month|date:"F Y" }}
+ {% endif %}
+ </p>
+
WeekArchiveView
---------------
@@ -208,6 +339,65 @@ WeekArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_week``.
+ **Example views.py**::
+
+ from django.views.generic.dates import WeekArchiveView
+
+ from myapp.models import Article
+
+ class ArticleWeekArchiveView(WeekArchiveView):
+ queryset = Article.objects.all()
+ date_field = "pub_date"
+ make_object_list = True
+ week_format = "%W"
+ allow_future = True
+
+ **Example urls.py**::
+
+ from django.conf.urls import patterns, url
+
+ from myapp.views import ArticleWeekArchiveView
+
+ urlpatterns = patterns('',
+ # Example: /2012/week/23/
+ url(r'^(?P<year>\d{4})/week/(?P<week>\d+)/$',
+ ArticleWeekArchiveView.as_view(),
+ name="archive_week"),
+ )
+
+ **Example myapp/article_archive_week.html**:
+
+ .. code-block:: html+django
+
+ <h1>Week {{ week|date:'W' }}</h1>
+
+ <ul>
+ {% for article in object_list %}
+ <li>{{ article.pub_date|date:"F j, Y" }}: {{ article.title }}</li>
+ {% endfor %}
+ </ul>
+
+ <p>
+ {% if previous_week %}
+ Previous Week: {{ previous_week|date:"F Y" }}
+ {% endif %}
+ {% if previous_week and next_week %}--{% endif %}
+ {% if next_week %}
+ Next week: {{ next_week|date:"F Y" }}
+ {% endif %}
+ </p>
+
+ In this example, you are outputting the week number. The default
+ ``week_format`` in the ``WeekArchiveView`` uses week format ``'%U'``
+ which is based on the United States week system where the week begins on a
+ Sunday. The ``'%W'`` format uses the ISO week format and its week
+ begins on a Monday. The ``'%W'`` format is the same in both the
+ :func:`~time.strftime` and the :tfilter:`date`.
+
+ However, the :tfilter:`date` template filter does not have an equivalent
+ output format that supports the US based week system. The :tfilter:`date`
+ filter ``'%U'`` outputs the number of seconds since the Unix epoch.
+
DayArchiveView
--------------
@@ -265,6 +455,53 @@ DayArchiveView
* Uses a default ``template_name_suffix`` of ``_archive_day``.
+ **Example views.py**::
+
+ from django.views.generic.dates import DayArchiveView
+
+ from myapp.models import Article
+
+ class ArticleDayArchiveView(DayArchiveView):
+ queryset = Article.objects.all()
+ date_field = "pub_date"
+ make_object_list = True
+ allow_future = True
+
+ **Example urls.py**::
+
+ from django.conf.urls import patterns, url
+
+ from myapp.views import ArticleDayArchiveView
+
+ urlpatterns = patterns('',
+ # Example: /2012/nov/10/
+ url(r'^(?P<year>\d{4})/(?P<month>[-\w]+)/(?P<day>\d+)/$',
+ ArticleDayArchiveView.as_view(),
+ name="archive_day"),
+ )
+
+ **Example myapp/article_archive_day.html**:
+
+ .. code-block:: html+django
+
+ <h1>{{ day }}</h1>
+
+ <ul>
+ {% for article in object_list %}
+ <li>{{ article.pub_date|date:"F j, Y" }}: {{ article.title }}</li>
+ {% endfor %}
+ </ul>
+
+ <p>
+ {% if previous_day %}
+ Previous Day: {{ previous_day }}
+ {% endif %}
+ {% if previous_day and next_day %}--{% endif %}
+ {% if next_day %}
+ Next Day: {{ next_day }}
+ {% endif %}
+ </p>
+
TodayArchiveView
----------------
@@ -289,6 +526,40 @@ TodayArchiveView
* :class:`django.views.generic.dates.DateMixin`
* :class:`django.views.generic.base.View`
+ **Notes**
+
+ * Uses a default ``template_name_suffix`` of ``_archive_today``.
+
+ **Example views.py**::
+
+ from django.views.generic.dates import TodayArchiveView
+
+ from myapp.models import Article
+
+ class ArticleTodayArchiveView(TodayArchiveView):
+ queryset = Article.objects.all()
+ date_field = "pub_date"
+ make_object_list = True
+ allow_future = True
+
+ **Example urls.py**::
+
+ from django.conf.urls import patterns, url
+
+ from myapp.views import ArticleTodayArchiveView
+
+ urlpatterns = patterns('',
+ url(r'^today/$',
+ ArticleTodayArchiveView.as_view(),
+ name="archive_today"),
+ )
+
+ .. admonition:: Where is the example template for ``TodayArchiveView``?
+
+ This view uses by default the same template as the
+ :class:`~DayArchiveView`, which is in the previous example. If you need
+ a different template, set the ``template_name`` attribute to be the
+ name of the new template.
DateDetailView
--------------
@@ -313,6 +584,32 @@ DateDetailView
* :class:`django.views.generic.detail.SingleObjectMixin`
* :class:`django.views.generic.base.View`
+ **Context**
+
+ * Includes the single object associated with the ``model`` specified in
+ the ``DateDetailView``.
+
+ **Notes**
+
+ * Uses a default ``template_name_suffix`` of ``_detail``.
+
+ **Example urls.py**::
+
+ from django.conf.urls import patterns, url
+ from django.views.generic.dates import DateDetailView
+
+ urlpatterns = patterns('',
+ url(r'^(?P<year>\d+)/(?P<month>[-\w]+)/(?P<day>\d+)/(?P<pk>\d+)/$',
+ DateDetailView.as_view(model=Article, date_field="pub_date"),
+ name="archive_date_detail"),
+ )
+
+ **Example myapp/article_detail.html**:
+
+ .. code-block:: html+django
+
+ <h1>{{ object.title }}</h1>
+
.. note::
All of the generic views listed above have matching ``Base`` views that
@@ -332,5 +629,3 @@ DateDetailView
.. class:: BaseTodayArchiveView
.. class:: BaseDateDetailView
-
-
diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt
index 2fac06ee02..7ce5c1d1be 100644
--- a/docs/ref/class-based-views/generic-editing.txt
+++ b/docs/ref/class-based-views/generic-editing.txt
@@ -12,12 +12,11 @@ editing content:
.. note::
- Some of the examples on this page assume that a model titled 'Author'
- has been defined. For these cases we assume the following has been defined
- in `myapp/models.py`::
+ Some of the examples on this page assume that an ``Article`` model has been
+ defined as follows in ``myapp/models.py``::
- from django import models
from django.core.urlresolvers import reverse
+ from django.db import models
class Author(models.Model):
name = models.CharField(max_length=200)
diff --git a/docs/ref/class-based-views/index.txt b/docs/ref/class-based-views/index.txt
index c4b632604a..a027953416 100644
--- a/docs/ref/class-based-views/index.txt
+++ b/docs/ref/class-based-views/index.txt
@@ -37,10 +37,11 @@ A class-based view is deployed into a URL pattern using the
is modified, the actions of one user visiting your view could have an
effect on subsequent users visiting the same view.
-Any argument passed into :meth:`~django.views.generic.base.View.as_view()` will
+Arguments passed into :meth:`~django.views.generic.base.View.as_view()` will
be assigned onto the instance that is used to service a request. Using the
previous example, this means that every request on ``MyView`` is able to use
-``self.size``.
+``self.size``. Arguments must correspond to attributes that already exist on
+the class (return ``True`` on a ``hasattr`` check).
Base vs Generic views
---------------------
diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt
index cdb743fcbd..c85c962bce 100644
--- a/docs/ref/class-based-views/mixins-multiple-object.txt
+++ b/docs/ref/class-based-views/mixins-multiple-object.txt
@@ -69,8 +69,27 @@ MultipleObjectMixin
An integer specifying how many objects should be displayed per page. If
this is given, the view will paginate objects with
:attr:`MultipleObjectMixin.paginate_by` objects per page. The view will
- expect either a ``page`` query string parameter (via ``GET``) or a
- ``page`` variable specified in the URLconf.
+ expect either a ``page`` query string parameter (via ``request.GET``)
+ or a ``page`` variable specified in the URLconf.
+
+ .. attribute:: paginate_orphans
+
+ .. versionadded:: 1.6
+
+ An integer specifying the number of "overflow" objects the last page
+ can contain. This extends the :attr:`MultipleObjectMixin.paginate_by`
+ limit on the last page by up to
+ :attr:`MultipleObjectMixin.paginate_orphans`, in order to keep the last
+ page from having a very small number of objects.
+
+ .. attribute:: page_kwarg
+
+ .. versionadded:: 1.5
+
+ A string specifying the name to use for the page parameter.
+ The view will expect this prameter to be available either as a query
+ string parameter (via ``request.GET``) or as a kwarg variable specified
+ in the URLconf. Defaults to ``page``.
.. attribute:: paginator_class
@@ -110,6 +129,14 @@ MultipleObjectMixin
Returns an instance of the paginator to use for this view. By default,
instantiates an instance of :attr:`paginator_class`.
+ .. method:: get_paginate_by()
+
+ .. versionadded:: 1.6
+
+ An integer specifying the number of "overflow" objects the last page
+ can contain. By default this simply returns the value of
+ :attr:`MultipleObjectMixin.paginate_orphans`.
+
.. method:: get_allow_empty()
Return a boolean specifying whether to display the page if no objects
diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt
index 3f3b52983b..d7eef623d5 100644
--- a/docs/ref/contrib/admin/actions.txt
+++ b/docs/ref/contrib/admin/actions.txt
@@ -140,6 +140,15 @@ That's really all there is to it! If you're itching to write your own actions,
you now know enough to get started. The rest of this document just covers more
advanced techniques.
+Handling errors in actions
+--------------------------
+
+If there are foreseeable error conditions that may occur while running your
+action, you should gracefully inform the user of the problem. This means
+handling exceptions and and using
+:meth:`django.contrib.admin.ModelAdmin.message_user` to display a user friendly
+description of the problem in the response.
+
Advanced action techniques
==========================
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index 6ed929cb7d..6f79e97a3c 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -26,9 +26,10 @@ There are seven steps in activating the Django admin site:
in your :setting:`INSTALLED_APPS` list, add them.
3. Add ``django.contrib.messages.context_processors.messages`` to
- :setting:`TEMPLATE_CONTEXT_PROCESSORS` and
- :class:`~django.contrib.messages.middleware.MessageMiddleware` to
- :setting:`MIDDLEWARE_CLASSES`. (These are both active by default, so
+ :setting:`TEMPLATE_CONTEXT_PROCESSORS` as well as
+ :class:`django.contrib.auth.middleware.AuthenticationMiddleware` and
+ :class:`django.contrib.messages.middleware.MessageMiddleware` to
+ :setting:`MIDDLEWARE_CLASSES`. (These are all active by default, so
you only need to do this if you've manually tweaked the settings.)
4. Determine which of your application's models should be editable in the
@@ -569,8 +570,8 @@ subclass::
.. image:: _images/users_changelist.png
- ``list_filter`` should be a list of elements, where each element should be
- of one of the following types:
+ ``list_filter`` should be a list or tuple of elements, where each element
+ should be of one of the following types:
* a field name, where the specified field should be either a
``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``,
@@ -815,15 +816,36 @@ subclass::
By default the admin shows all fields as editable. Any fields in this
option (which should be a ``list`` or ``tuple``) will display its data
- as-is and non-editable. This option behaves nearly identical to
- :attr:`ModelAdmin.list_display`. Usage is the same, however, when you
- specify :attr:`ModelAdmin.fields` or :attr:`ModelAdmin.fieldsets` the
- read-only fields must be present to be shown (they are ignored otherwise).
+ as-is and non-editable; they are also excluded from the
+ :class:`~django.forms.ModelForm` used for creating and editing. Note that
+ when specifying :attr:`ModelAdmin.fields` or :attr:`ModelAdmin.fieldsets`
+ the read-only fields must be present to be shown (they are ignored
+ otherwise).
If ``readonly_fields`` is used without defining explicit ordering through
:attr:`ModelAdmin.fields` or :attr:`ModelAdmin.fieldsets` they will be
added last after all editable fields.
+ A read-only field can not only display data from a model's field, it can
+ also display the output of a a model's method or a method of the
+ ``ModelAdmin`` class itself. This is very similar to the way
+ :attr:`ModelAdmin.list_display` behaves. This provides an easy way to use
+ the admin interface to provide feedback on the status of the objects being
+ edited, for example::
+
+ class PersonAdmin(ModelAdmin):
+ readonly_fields = ('address_report',)
+
+ def address_report(self, instance):
+ return ", ".join(instance.get_full_address()) or \
+ "<span class='errors'>I can't determine this address.</span>"
+
+ # short_description functions like a model field's verbose_name
+ address_report.short_description = "Address"
+ # in this example, we have used HTML tags in the output
+ address_report.allow_tags = True
+
+
.. attribute:: ModelAdmin.save_as
Set ``save_as`` to enable a "save as" feature on admin change forms.
@@ -1054,6 +1076,14 @@ templates used by the :class:`ModelAdmin` views:
changelist that will be linked to the change view, as described in the
:attr:`ModelAdmin.list_display_links` section.
+.. method:: ModelAdmin.get_list_filter(self, request)
+
+ .. versionadded:: 1.5
+
+ The ``get_list_filter`` method is given the ``HttpRequest`` and is expected
+ to return the same kind of sequence type as for the
+ :attr:`~ModelAdmin.list_filter` attribute.
+
.. method:: ModelAdmin.get_inline_instances(self, request, obj=None)
.. versionadded:: 1.5
@@ -1213,10 +1243,39 @@ templates used by the :class:`ModelAdmin` views:
.. method:: ModelAdmin.get_changelist(self, request, **kwargs)
- Returns the Changelist class to be used for listing. By default,
+ Returns the ``Changelist`` class to be used for listing. By default,
``django.contrib.admin.views.main.ChangeList`` is used. By inheriting this
class you can change the behavior of the listing.
+.. method:: ModelAdmin.get_changelist_form(self, request, **kwargs)
+
+ Returns a :class:`~django.forms.ModelForm` class for use in the ``Formset``
+ on the changelist page. To use a custom form, for example::
+
+ class MyForm(forms.ModelForm):
+ class Meta:
+ model = MyModel
+
+ class MyModelAdmin(admin.ModelAdmin):
+ def get_changelist_form(self, request, **kwargs):
+ return MyForm
+
+.. method:: ModelAdmin.get_changelist_formset(self, request, **kwargs)
+
+ Returns a :ref:`ModelFormSet <model-formsets>` class for use on the
+ changelist page if :attr:`~ModelAdmin.list_editable` is used. To use a
+ custom formset, for example::
+
+ from django.forms.models import BaseModelFormSet
+
+ class MyAdminFormSet(BaseModelFormSet):
+ pass
+
+ class MyModelAdmin(admin.ModelAdmin):
+ def get_changelist_formset(self, request, **kwargs):
+ kwargs['formset'] = MyAdminFormSet
+ return super(MyModelAdmin, self).get_changelist_formset(request, **kwargs)
+
.. method:: ModelAdmin.has_add_permission(self, request)
Should return ``True`` if adding an object is permitted, ``False``
@@ -1252,11 +1311,19 @@ templates used by the :class:`ModelAdmin` views:
return qs
return qs.filter(author=request.user)
-.. method:: ModelAdmin.message_user(request, message)
+.. method:: ModelAdmin.message_user(request, message, level=messages.INFO, extra_tags='', fail_silently=False)
+
+ Sends a message to the user using the :mod:`django.contrib.messages`
+ backend. See the :ref:`custom ModelAdmin example <custom-admin-action>`.
+
+ .. versionadded:: 1.5
- Sends a message to the user. The default implementation creates a message
- using the :mod:`django.contrib.messages` backend. See the
- :ref:`custom ModelAdmin example <custom-admin-action>`.
+ Keyword arguments allow you to change the message level, add extra CSS
+ tags, or fail silently if the ``contrib.messages`` framework is not
+ installed. These keyword arguments match those for
+ :func:`django.contrib.messages.add_message`, see that function's
+ documentation for more details. One difference is that the level may be
+ passed as a string label in addition to integer/constant.
.. method:: ModelAdmin.get_paginator(queryset, per_page, orphans=0, allow_empty_first_page=True)
@@ -1326,6 +1393,8 @@ instances which allow you to easily customize the response data before
rendering. For more details, see the
:doc:`TemplateResponse documentation </ref/template-response>`.
+.. _modeladmin-media-definitions:
+
``ModelAdmin`` media definitions
--------------------------------
@@ -1532,6 +1601,10 @@ The ``InlineModelAdmin`` class adds:
Specifies whether or not inline objects can be deleted in the inline.
Defaults to ``True``.
+.. method:: InlineModelAdmin.get_formset(self, request, obj=None, **kwargs)
+
+ Returns a ``BaseInlineFormSet`` class for use in admin add/change views.
+ See the example for :class:`ModelAdmin.get_formsets`.
Working with a model with two or more foreign keys to the same parent model
---------------------------------------------------------------------------
diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt
index 1c6ff7c7ed..8275092d2f 100644
--- a/docs/ref/contrib/comments/index.txt
+++ b/docs/ref/contrib/comments/index.txt
@@ -11,12 +11,6 @@ Django includes a simple, yet customizable comments framework. The built-in
comments framework can be used to attach comments to any model, so you can use
it for comments on blog entries, photos, book chapters, or anything else.
-.. note::
-
- If you used to use Django's older (undocumented) comments framework, you'll
- need to upgrade. See the :doc:`upgrade guide </ref/contrib/comments/upgrade>`
- for instructions.
-
Quick start guide
=================
@@ -209,7 +203,7 @@ default version of which is included with Django.
Rendering a custom comment form
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-If you want more control over the look and feel of the comment form, you use use
+If you want more control over the look and feel of the comment form, you may use
:ttag:`get_comment_form` to get a :doc:`form object </topics/forms/index>` that
you can use in the template::
@@ -350,7 +344,6 @@ More information
models
settings
signals
- upgrade
custom
forms
moderation
diff --git a/docs/ref/contrib/comments/signals.txt b/docs/ref/contrib/comments/signals.txt
index 9d7c435927..8274539ed7 100644
--- a/docs/ref/contrib/comments/signals.txt
+++ b/docs/ref/contrib/comments/signals.txt
@@ -20,8 +20,8 @@ Sent just before a comment will be saved, after it's been sanity checked and
submitted. This can be used to modify the comment (in place) with posting
details or other such actions.
-If any receiver returns ``False`` the comment will be discarded and a 403 (not
-allowed) response will be returned.
+If any receiver returns ``False`` the comment will be discarded and a 400
+response will be returned.
This signal is sent at more or less the same time (just before, actually) as the
``Comment`` object's :data:`~django.db.models.signals.pre_save` signal.
diff --git a/docs/ref/contrib/comments/upgrade.txt b/docs/ref/contrib/comments/upgrade.txt
deleted file mode 100644
index dadb53f5fa..0000000000
--- a/docs/ref/contrib/comments/upgrade.txt
+++ /dev/null
@@ -1,78 +0,0 @@
-===============================================
-Upgrading from Django's previous comment system
-===============================================
-
-Prior versions of Django included an outdated, undocumented comment system. Users who reverse-engineered this framework will need to upgrade to use the
-new comment system; this guide explains how.
-
-The main changes from the old system are:
-
-* This new system is documented.
-
-* It uses modern Django features like :doc:`forms </topics/forms/index>` and
- :doc:`modelforms </topics/forms/modelforms>`.
-
-* It has a single ``Comment`` model instead of separate ``FreeComment`` and
- ``Comment`` models.
-
-* Comments have "email" and "URL" fields.
-
-* No ratings, photos and karma. This should only effect World Online.
-
-* The ``{% comment_form %}`` tag no longer exists. Instead, there's now two
- functions: ``{% get_comment_form %}``, which returns a form for posting a
- new comment, and ``{% render_comment_form %}``, which renders said form
- using the ``comments/form.html`` template.
-
-* The way comments are include in your URLconf have changed; you'll need to
- replace::
-
- (r'^comments/', include('django.contrib.comments.urls.comments')),
-
- with::
-
- (r'^comments/', include('django.contrib.comments.urls')),
-
-Upgrading data
---------------
-
-The data models for Django's comment system have changed, as have the
-table names. Before you transfer your existing data into the new comments
-system, make sure that you have installed the new comments system as
-explained in the
-:doc:`quick start guide </ref/contrib/comments/index>`.
-This will ensure that the new tables have been properly created.
-
-To transfer your data into the new comments system, you'll need to directly
-run the following SQL:
-
-.. code-block:: sql
-
- BEGIN;
-
- INSERT INTO django_comments
- (content_type_id, object_pk, site_id, user_name, user_email, user_url,
- comment, submit_date, ip_address, is_public, is_removed)
- SELECT
- content_type_id, object_id, site_id, person_name, '', '', comment,
- submit_date, ip_address, is_public, not approved
- FROM comments_freecomment;
-
- INSERT INTO django_comments
- (content_type_id, object_pk, site_id, user_id, user_name, user_email,
- user_url, comment, submit_date, ip_address, is_public, is_removed)
- SELECT
- content_type_id, object_id, site_id, user_id, '', '', '', comment,
- submit_date, ip_address, is_public, is_removed
- FROM comments_comment;
-
- UPDATE django_comments SET user_name = (
- SELECT username FROM auth_user
- WHERE django_comments.user_id = auth_user.id
- ) WHERE django_comments.user_id is not NULL;
- UPDATE django_comments SET user_email = (
- SELECT email FROM auth_user
- WHERE django_comments.user_id = auth_user.id
- ) WHERE django_comments.user_id is not NULL;
-
- COMMIT;
diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt
index 0ced1bf155..3edc019d05 100644
--- a/docs/ref/contrib/formtools/form-wizard.txt
+++ b/docs/ref/contrib/formtools/form-wizard.txt
@@ -493,6 +493,21 @@ Advanced ``WizardView`` methods
context = self.get_context_data(form=form, **kwargs)
return self.render_to_response(context)
+.. method:: WizardView.get_cleaned_data_for_step(step)
+
+ This method returns the cleaned data for a given ``step``. Before returning
+ the cleaned data, the stored values are revalidated through the form. If
+ the data doesn't validate, ``None`` will be returned.
+
+.. method:: WizardView.get_all_cleaned_data()
+
+ This method returns a merged dictionary of all form steps' ``cleaned_data``
+ dictionaries. If a step contains a ``FormSet``, the key will be prefixed
+ with ``formset-`` and contain a list of the formset's ``cleaned_data``
+ dictionaries. Note that if two or more steps have a field with the same
+ name, the value for that field from the latest step will overwrite the
+ value from any earlier steps.
+
Providing initial data for the forms
====================================
@@ -534,6 +549,16 @@ This storage will temporarily store the uploaded files for the wizard. The
:attr:`file_storage` attribute should be a
:class:`~django.core.files.storage.Storage` subclass.
+Django provides a built-in storage class (see :ref:`the built-in filesystem
+storage class <builtin-fs-storage>`)::
+
+ from django.conf import settings
+ from django.core.files.storage import FileSystemStorage
+
+ class CustomWizardView(WizardView):
+ ...
+ file_storage = FileSystemStorage(location=os.path.join(settings.MEDIA_ROOT, 'photos'))
+
.. warning::
Please remember to take care of removing old files as the
@@ -622,8 +647,11 @@ Usage of ``NamedUrlWizardView``
.. class:: NamedUrlWizardView
-There is a :class:`WizardView` subclass which adds named-urls support to the wizard.
-By doing this, you can have single urls for every step.
+There is a :class:`WizardView` subclass which adds named-urls support to the
+wizard. By doing this, you can have single urls for every step. You can also
+use the :class:`NamedUrlSessionWizardView` or :class:`NamedUrlCookieWizardView`
+classes which preselect the backend used for storing information (server-side
+sessions and browser cookies respectively).
To use the named urls, you have to change the ``urls.py``.
diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt
index eb20b1f411..7d7c32781c 100644
--- a/docs/ref/contrib/gis/geos.txt
+++ b/docs/ref/contrib/gis/geos.txt
@@ -656,6 +656,17 @@ is returned instead.
Returns the number of interior rings in this geometry.
+.. admonition:: Comparing Polygons
+
+ Note that it is possible to compare ``Polygon`` objects directly with ``<``
+ or ``>``, but as the comparison is made through Polygon's
+ :class:`LineString`, it does not mean much (but is consistent and quick).
+ You can always force the comparison with the :attr:`~GEOSGeometry.area`
+ property::
+
+ >>> if poly_1.area > poly_2.area:
+ >>> pass
+
Geometry Collections
====================
diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt
index c710866813..100dc2edd0 100644
--- a/docs/ref/contrib/gis/install/index.txt
+++ b/docs/ref/contrib/gis/install/index.txt
@@ -118,7 +118,7 @@ Invoke the Django shell from your project and execute the
.. code-block:: pycon
- $ python manage shell
+ $ python manage.py shell
>>> from django.contrib.gis.utils import add_srs_entry
>>> add_srs_entry(900913)
diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt
index ec265342b3..5000622ad4 100644
--- a/docs/ref/contrib/gis/tutorial.txt
+++ b/docs/ref/contrib/gis/tutorial.txt
@@ -5,28 +5,28 @@ GeoDjango Tutorial
Introduction
============
-GeoDjango is an add-on for Django that turns it into a world-class geographic
-Web framework. GeoDjango strives to make it as simple as possible to create
-geographic Web applications, like location-based services. Some features
-include:
+GeoDjango is an included contrib module for Django that turns it into a
+world-class geographic Web framework. GeoDjango strives to make it as simple
+as possible to create geographic Web applications, like location-based services.
+Its features include:
* Django model fields for `OGC`_ geometries.
-* Extensions to Django's ORM for the querying and manipulation of spatial data.
+* Extensions to Django's ORM for querying and manipulating spatial data.
* Loosely-coupled, high-level Python interfaces for GIS geometry operations and
data formats.
-* Editing of geometry fields inside the admin.
+* Editing geometry fields from the admin.
-This tutorial assumes a familiarity with Django; thus, if you're brand new to
-Django please read through the :doc:`regular tutorial </intro/tutorial01>` to
-introduce yourself with basic Django concepts.
+This tutorial assumes familiarity with Django; thus, if you're brand new to
+Django, please read through the :doc:`regular tutorial </intro/tutorial01>` to
+familiarize yourself with Django first.
.. note::
- GeoDjango has special prerequisites overwhat is required by Django --
+ GeoDjango has additional requirements beyond what Django requires --
please consult the :ref:`installation documentation <ref-gis-install>`
for more details.
-This tutorial will guide you through the creation of a geographic Web
+This tutorial will guide you through the creation of a geographic web
application for viewing the `world borders`_. [#]_ Some of the code
used in this tutorial is taken from and/or inspired by the `GeoDjango
basic apps`_ project. [#]_
@@ -51,10 +51,10 @@ Create a Spatial Database
MySQL and Oracle users can skip this section because spatial types
are already built into the database.
-First, a spatial database needs to be created for our project. If using
-PostgreSQL and PostGIS, then the following commands will
-create the database from a :ref:`spatial database template
-<spatialdb_template>`:
+First, create a spatial database for your project.
+
+If you are using PostGIS, create the database from the :ref:`spatial database
+template <spatialdb_template>`:
.. code-block:: bash
@@ -62,9 +62,9 @@ create the database from a :ref:`spatial database template
.. note::
- This command must be issued by a database user that has permissions to
- create a database. Here is an example set of commands to create such
- a user:
+ This command must be issued by a database user with enough privileges to
+ create a database. To create a user with ``CREATE DATABASE`` privileges in
+ PostgreSQL, use the following commands:
.. code-block:: bash
@@ -72,25 +72,24 @@ create the database from a :ref:`spatial database template
$ createuser --createdb geo
$ exit
- Replace ``geo`` with the system login user name that will be
- connecting to the database. For example, ``johndoe`` if that is the
- system user that will be running GeoDjango.
+ Replace ``geo`` with your Postgres database user's username.
+ (In PostgreSQL, this user will also be an OS-level user.)
-Users of SQLite and SpatiaLite should consult the instructions on how
+If you are using SQLite and SpatiaLite, consult the instructions on how
to create a :ref:`SpatiaLite database <create_spatialite_db>`.
-Create GeoDjango Project
+Create a New Project
------------------------
-Use the ``django-admin.py`` script like normal to create a ``geodjango``
-project:
+Use the standard ``django-admin.py`` script to create a project called
+``geodjango``:
.. code-block:: bash
$ django-admin.py startproject geodjango
-With the project initialized, now create a ``world`` Django application within
-the ``geodjango`` project:
+This will initialize a new project. Now, create a ``world`` Django application
+within the ``geodjango`` project:
.. code-block:: bash
@@ -101,7 +100,7 @@ Configure ``settings.py``
-------------------------
The ``geodjango`` project settings are stored in the ``geodjango/settings.py``
-file. Edit the database connection settings appropriately::
+file. Edit the database connection settings to match your setup::
DATABASES = {
'default': {
@@ -113,7 +112,7 @@ file. Edit the database connection settings appropriately::
In addition, modify the :setting:`INSTALLED_APPS` setting to include
:mod:`django.contrib.admin`, :mod:`django.contrib.gis`,
-and ``world`` (our newly created application)::
+and ``world`` (your newly created application)::
INSTALLED_APPS = (
'django.contrib.auth',
@@ -135,9 +134,9 @@ Geographic Data
World Borders
-------------
-The world borders data is available in this `zip file`__. Create a data
+The world borders data is available in this `zip file`__. Create a ``data``
directory in the ``world`` application, download the world borders data, and
-unzip. On GNU/Linux platforms the following commands should do it:
+unzip. On GNU/Linux platforms, use the following commands:
.. code-block:: bash
@@ -149,7 +148,7 @@ unzip. On GNU/Linux platforms the following commands should do it:
The world borders ZIP file contains a set of data files collectively known as
an `ESRI Shapefile`__, one of the most popular geospatial data formats. When
-unzipped the world borders data set includes files with the following
+unzipped, the world borders dataset includes files with the following
extensions:
* ``.shp``: Holds the vector data for the world borders geometries.
@@ -165,8 +164,8 @@ __ http://en.wikipedia.org/wiki/Shapefile
Use ``ogrinfo`` to examine spatial data
---------------------------------------
-The GDAL ``ogrinfo`` utility is excellent for examining metadata about
-shapefiles (or other vector data sources):
+The GDAL ``ogrinfo`` utility allows examining the metadata of shapefiles or
+other vector data sources:
.. code-block:: bash
@@ -175,9 +174,9 @@ shapefiles (or other vector data sources):
using driver `ESRI Shapefile' successful.
1: TM_WORLD_BORDERS-0.3 (Polygon)
-Here ``ogrinfo`` is telling us that the shapefile has one layer, and that such
-layer contains polygon data. To find out more we'll specify the layer name
-and use the ``-so`` option to get only important summary information:
+``ogrinfo`` tells us that the shapefile has one layer, and that this
+layer contains polygon data. To find out more, we'll specify the layer name
+and use the ``-so`` option to get only the important summary information:
.. code-block:: bash
@@ -208,14 +207,11 @@ and use the ``-so`` option to get only important summary information:
LAT: Real (7.3)
This detailed summary information tells us the number of features in the layer
-(246), the geographical extent, the spatial reference system ("SRS WKT"),
-as well as detailed information for each attribute field. For example,
-``FIPS: String (2.0)`` indicates that there's a ``FIPS`` character field
-with a maximum length of 2; similarly, ``LON: Real (8.3)`` is a floating-point
-field that holds a maximum of 8 digits up to three decimal places. Although
-this information may be found right on the `world borders`_ Web site, this
-shows you how to determine this information yourself when such metadata is not
-provided.
+(246), the geographic bounds of the data, the spatial reference system
+("SRS WKT"), as well as type information for each attribute field. For example,
+``FIPS: String (2.0)`` indicates that the ``FIPS`` character field has
+a maximum length of 2. Similarly, ``LON: Real (8.3)`` is a floating-point
+field that holds a maximum of 8 digits up to three decimal places.
Geographic Models
=================
@@ -223,8 +219,8 @@ Geographic Models
Defining a Geographic Model
---------------------------
-Now that we've examined our world borders data set using ``ogrinfo``, we can
-create a GeoDjango model to represent this data::
+Now that you've examined your dataset using ``ogrinfo``, create a GeoDjango
+model to represent this data::
from django.contrib.gis.db import models
@@ -252,32 +248,30 @@ create a GeoDjango model to represent this data::
def __unicode__(self):
return self.name
-Two important things to note:
+Please note two important things:
1. The ``models`` module is imported from :mod:`django.contrib.gis.db`.
-2. The model overrides its default manager with
- :class:`~django.contrib.gis.db.models.GeoManager`; this is *required*
- to perform spatial queries.
+2. You must override the model's default manager with
+ :class:`~django.contrib.gis.db.models.GeoManager` to perform spatial queries.
-When declaring a geometry field on your model the default spatial reference
-system is WGS84 (meaning the `SRID`__ is 4326) -- in other words, the field
-coordinates are in longitude/latitude pairs in units of degrees. If you want
-the coordinate system to be different, then SRID of the geometry field may be
-customized by setting the ``srid`` with an integer corresponding to the
-coordinate system of your choice.
+The default spatial reference system for geometry fields is WGS84 (meaning
+the `SRID`__ is 4326) -- in other words, the field coordinates are in
+longitude, latitude pairs in units of degrees. To use a different
+coordinate system, set the SRID of the geometry field with the ``srid``
+argument. Use an integer representing the coordinate system's EPSG code.
__ http://en.wikipedia.org/wiki/SRID
Run ``syncdb``
--------------
-After you've defined your model, it needs to be synced with the spatial
-database. First, let's look at the SQL that will generate the table for the
+After defining your model, you need to sync it with the database. First,
+let's look at the SQL that will generate the table for the
``WorldBorder`` model::
$ python manage.py sqlall world
-This management command should produce the following output:
+This command should produce the following output:
.. code-block:: sql
@@ -302,32 +296,28 @@ This management command should produce the following output:
CREATE INDEX "world_worldborder_mpoly_id" ON "world_worldborder" USING GIST ( "mpoly" GIST_GEOMETRY_OPS );
COMMIT;
-If satisfied, you may then create this table in the database by running the
-``syncdb`` management command::
+If this looks correct, run ``syncdb`` to create this table in the database::
$ python manage.py syncdb
Creating table world_worldborder
Installing custom SQL for world.WorldBorder model
-The ``syncdb`` command may also prompt you to create an admin user; go ahead
-and do so (not required now, may be done at any point in the future using the
-``createsuperuser`` management command).
+The ``syncdb`` command may also prompt you to create an admin user. Either
+do so now, or later by running ``django-admin.py createsuperuser``.
Importing Spatial Data
======================
-This section will show you how to take the data from the world borders
-shapefile and import it into GeoDjango models using the
+This section will show you how to import the world borders
+shapefile into the database via GeoDjango models using the
:ref:`ref-layermapping`.
-There are many different ways to import data in to a spatial database --
-besides the tools included within GeoDjango, you may also use the following to
-populate your spatial database:
+There are many different ways to import data into a spatial database --
+besides the tools included within GeoDjango, you may also use the following:
-* `ogr2ogr`_: Command-line utility, included with GDAL, that
- supports loading a multitude of vector data formats into
- the PostGIS, MySQL, and Oracle spatial databases.
-* `shp2pgsql`_: This utility is included with PostGIS and only supports
- ESRI shapefiles.
+* `ogr2ogr`_: A command-line utility included with GDAL that
+ can import many vector data formats into PostGIS, MySQL, and Oracle databases.
+* `shp2pgsql`_: This utility included with PostGIS imports ESRI shapefiles into
+ PostGIS.
.. _ogr2ogr: http://www.gdal.org/ogr2ogr.html
.. _shp2pgsql: http://postgis.refractions.net/documentation/manual-1.5/ch04.html#shp2pgsql_usage
@@ -337,10 +327,9 @@ populate your spatial database:
GDAL Interface
--------------
-Earlier we used the ``ogrinfo`` to explore the contents of the world borders
-shapefile. Included within GeoDjango is an interface to GDAL's powerful OGR
-library -- in other words, you'll be able explore all the vector data sources
-that OGR supports via a Pythonic API.
+Earlier, you used ``ogrinfo`` to examine the contents of the world borders
+shapefile. GeoDjango also includes a Pythonic interface to GDAL's powerful OGR
+library that can work with all the vector data sources that OGR supports.
First, invoke the Django shell:
@@ -348,8 +337,8 @@ First, invoke the Django shell:
$ python manage.py shell
-If the :ref:`worldborders` data was downloaded like earlier in the
-tutorial, then we can determine the path using Python's built-in
+If you downloaded the :ref:`worldborders` data earlier in the
+tutorial, then you can determine its path using Python's built-in
``os`` module::
>>> import os
@@ -357,7 +346,7 @@ tutorial, then we can determine the path using Python's built-in
>>> world_shp = os.path.abspath(os.path.join(os.path.dirname(world.__file__),
... 'data/TM_WORLD_BORDERS-0.3.shp'))
-Now, the world borders shapefile may be opened using GeoDjango's
+Now, open the world borders shapefile using GeoDjango's
:class:`~django.contrib.gis.gdal.DataSource` interface::
>>> from django.contrib.gis.gdal import DataSource
@@ -374,8 +363,7 @@ shapefiles are only allowed to have one layer::
>>> print(lyr)
TM_WORLD_BORDERS-0.3
-You can see what the geometry type of the layer is and how many features it
-contains::
+You can see the layer's geometry type and how many features it contains::
>>> print(lyr.geom_type)
Polygon
@@ -384,16 +372,16 @@ contains::
.. note::
- Unfortunately the shapefile data format does not allow for greater
+ Unfortunately, the shapefile data format does not allow for greater
specificity with regards to geometry types. This shapefile, like
- many others, actually includes ``MultiPolygon`` geometries in its
- features. You need to watch out for this when creating your models
- as a GeoDjango ``PolygonField`` will not accept a ``MultiPolygon``
- type geometry -- thus a ``MultiPolygonField`` is used in our model's
- definition instead.
+ many others, actually includes ``MultiPolygon`` geometries, not Polygons.
+ It's important to use a more general field type in models: a
+ GeoDjango ``MultiPolygonField`` will accept a ``Polygon`` geometry, but a
+ ``PolygonField`` will not accept a ``MultiPolygon`` type geometry. This
+ is why the ``WorldBorder`` model defined above uses a ``MultiPolygonField``.
The :class:`~django.contrib.gis.gdal.Layer` may also have a spatial reference
-system associated with it -- if it does, the ``srs`` attribute will return a
+system associated with it. If it does, the ``srs`` attribute will return a
:class:`~django.contrib.gis.gdal.SpatialReference` object::
>>> srs = lyr.srs
@@ -406,9 +394,9 @@ system associated with it -- if it does, the ``srs`` attribute will return a
>>> srs.proj4 # PROJ.4 representation
'+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs '
-Here we've noticed that the shapefile is in the popular WGS84 spatial reference
-system -- in other words, the data uses units of degrees longitude and
-latitude.
+This shapefile is in the popular WGS84 spatial reference
+system -- in other words, the data uses longitude, latitude pairs in
+units of degrees.
In addition, shapefiles also support attribute fields that may contain
additional data. Here are the fields on the World Borders layer:
@@ -416,8 +404,8 @@ additional data. Here are the fields on the World Borders layer:
>>> print(lyr.fields)
['FIPS', 'ISO2', 'ISO3', 'UN', 'NAME', 'AREA', 'POP2005', 'REGION', 'SUBREGION', 'LON', 'LAT']
-Here we are examining the OGR types (e.g., whether a field is an integer or
-a string) associated with each of the fields:
+The following code will let you examine the OGR types (e.g. integer or
+string) associated with each of the fields:
>>> [fld.__name__ for fld in lyr.field_types]
['OFTString', 'OFTString', 'OFTString', 'OFTInteger', 'OFTString', 'OFTInteger', 'OFTInteger', 'OFTInteger', 'OFTInteger', 'OFTReal', 'OFTReal']
@@ -446,8 +434,7 @@ And individual features may be retrieved by their feature ID::
>>> print(feat.get('NAME'))
San Marino
-Here the boundary geometry for San Marino is extracted and looking
-exported to WKT and GeoJSON::
+Boundary geometries may be exported as WKT and GeoJSON::
>>> geom = feat.geom
>>> print(geom.wkt)
@@ -459,8 +446,9 @@ exported to WKT and GeoJSON::
``LayerMapping``
----------------
-We're going to dive right in -- create a file called ``load.py`` inside the
-``world`` application, and insert the following::
+To import the data, use a LayerMapping in a Python script.
+Create a file called ``load.py`` inside the ``world`` application,
+with the following code::
import os
from django.contrib.gis.utils import LayerMapping
@@ -492,20 +480,20 @@ We're going to dive right in -- create a file called ``load.py`` inside the
A few notes about what's going on:
* Each key in the ``world_mapping`` dictionary corresponds to a field in the
- ``WorldBorder`` model, and the value is the name of the shapefile field
+ ``WorldBorder`` model. The value is the name of the shapefile field
that data will be loaded from.
* The key ``mpoly`` for the geometry field is ``MULTIPOLYGON``, the
- geometry type we wish to import as. Even if simple polygons are encountered
- in the shapefile they will automatically be converted into collections prior
- to insertion into the database.
+ geometry type GeoDjango will import the field as. Even simple polygons in
+ the shapefile will automatically be converted into collections prior to
+ insertion into the database.
* The path to the shapefile is not absolute -- in other words, if you move the
``world`` application (with ``data`` subdirectory) to a different location,
- then the script will still work.
+ the script will still work.
* The ``transform`` keyword is set to ``False`` because the data in the
shapefile does not need to be converted -- it's already in WGS84 (SRID=4326).
-* The ``encoding`` keyword is set to the character encoding of string values in
- the shapefile. This ensures that string values are read and saved correctly
- from their original encoding system.
+* The ``encoding`` keyword is set to the character encoding of the string
+ values in the shapefile. This ensures that string values are read and saved
+ correctly from their original encoding system.
Afterwards, invoke the Django shell from the ``geodjango`` project directory:
@@ -513,8 +501,8 @@ Afterwards, invoke the Django shell from the ``geodjango`` project directory:
$ python manage.py shell
-Next, import the ``load`` module, call the ``run`` routine, and watch ``LayerMapping``
-do the work::
+Next, import the ``load`` module, call the ``run`` routine, and watch
+``LayerMapping`` do the work::
>>> from world import load
>>> load.run()
@@ -536,7 +524,7 @@ The general usage of the command goes as follows:
$ python manage.py ogrinspect [options] <data_source> <model_name> [options]
-Where ``data_source`` is the path to the GDAL-supported data source and
+``data_source`` is the path to the GDAL-supported data source and
``model_name`` is the name to use for the model. Command-line options may
be used to further define how the model is generated.
@@ -600,9 +588,9 @@ Spatial Queries
Spatial Lookups
---------------
-GeoDjango extends the Django ORM and allows the use of spatial lookups.
-Let's do an example where we find the ``WorldBorder`` model that contains
-a point. First, fire up the management shell:
+GeoDjango adds spatial lookups to the Django ORM. For example, you
+can find the country in the ``WorldBorder`` table that contains
+a particular point. First, fire up the management shell:
.. code-block:: bash
@@ -613,8 +601,8 @@ Now, define a point of interest [#]_::
>>> pnt_wkt = 'POINT(-95.3385 29.7245)'
The ``pnt_wkt`` string represents the point at -95.3385 degrees longitude,
-and 29.7245 degrees latitude. The geometry is in a format known as
-Well Known Text (WKT), an open standard issued by the Open Geospatial
+29.7245 degrees latitude. The geometry is in a format known as
+Well Known Text (WKT), a standard issued by the Open Geospatial
Consortium (OGC). [#]_ Import the ``WorldBorder`` model, and perform
a ``contains`` lookup using the ``pnt_wkt`` as the parameter::
@@ -623,11 +611,13 @@ a ``contains`` lookup using the ``pnt_wkt`` as the parameter::
>>> qs
[<WorldBorder: United States>]
-Here we retrieved a ``GeoQuerySet`` that has only one model: the one
-for the United States (which is what we would expect). Similarly,
-a :ref:`GEOS geometry object <ref-geos>` may also be used -- here the
-``intersects`` spatial lookup is combined with the ``get`` method to retrieve
-only the ``WorldBorder`` instance for San Marino instead of a queryset::
+Here, you retrieved a ``GeoQuerySet`` with only one model: the border of
+the United States (exactly what you would expect).
+
+Similarly, you may also use a :ref:`GEOS geometry object <ref-geos>`.
+Here, you can combine the ``intersects`` spatial lookup with the ``get``
+method to retrieve only the ``WorldBorder`` instance for San Marino instead
+of a queryset::
>>> from django.contrib.gis.geos import Point
>>> pnt = Point(12.4604, 43.9420)
@@ -635,16 +625,16 @@ only the ``WorldBorder`` instance for San Marino instead of a queryset::
>>> sm
<WorldBorder: San Marino>
-The ``contains`` and ``intersects`` lookups are just a subset of what's
-available -- the :ref:`ref-gis-db-api` documentation has more.
+The ``contains`` and ``intersects`` lookups are just a subset of the
+available queries -- the :ref:`ref-gis-db-api` documentation has more.
Automatic Spatial Transformations
---------------------------------
-When querying the spatial database GeoDjango automatically transforms
+When doing spatial queries, GeoDjango automatically transforms
geometries if they're in a different coordinate system. In the following
-example, the coordinate will be expressed in terms of `EPSG SRID 32140`__,
+example, coordinates will be expressed in `EPSG SRID 32140`__,
a coordinate system specific to south Texas **only** and in units of
-**meters** and not degrees::
+**meters**, not degrees::
>>> from django.contrib.gis.geos import Point, GEOSGeometry
>>> pnt = Point(954158.1, 4215137.1, srid=32140)
@@ -654,7 +644,7 @@ WKT that includes the SRID::
>>> pnt = GEOSGeometry('SRID=32140;POINT(954158.1 4215137.1)')
-When using GeoDjango's ORM, it will automatically wrap geometry values
+GeoDjango's ORM will automatically wrap geometry values
in transformation SQL, allowing the developer to work at a higher level
of abstraction::
@@ -675,7 +665,7 @@ __ http://spatialreference.org/ref/epsg/32140/
When using :doc:`raw queries </topics/db/sql>`, you should generally wrap
your geometry fields with the ``asText()`` SQL function (or ``ST_AsText``
- for PostGIS) so as the field value will be recognized by GEOS::
+ for PostGIS) so that the field value will be recognized by GEOS::
City.objects.raw('SELECT id, name, asText(point) from myapp_city')
@@ -684,8 +674,8 @@ __ http://spatialreference.org/ref/epsg/32140/
Lazy Geometries
---------------
-Geometries come to GeoDjango in a standardized textual representation. Upon
-access of the geometry field, GeoDjango creates a `GEOS geometry object
+GeoDjango loads geometries in a standardized textual representation. When the
+geometry field is first accessed, GeoDjango creates a `GEOS geometry object
<ref-geos>`, exposing powerful functionality, such as serialization properties
for popular geospatial formats::
@@ -715,14 +705,11 @@ the GEOS library::
Putting your data on the map
============================
-Google
-------
-
Geographic Admin
----------------
GeoDjango extends :doc:`Django's admin application </ref/contrib/admin/index>`
-to enable support for editing geometry fields.
+with support for editing geometry fields.
Basics
^^^^^^
@@ -730,16 +717,15 @@ Basics
GeoDjango also supplements the Django admin by allowing users to create
and modify geometries on a JavaScript slippy map (powered by `OpenLayers`_).
-Let's dive in again -- create a file called ``admin.py`` inside the
-``world`` application, and insert the following::
+Let's dive right in. Create a file called ``admin.py`` inside the
+``world`` application with the following code::
from django.contrib.gis import admin
from models import WorldBorder
admin.site.register(WorldBorder, admin.GeoModelAdmin)
-Next, edit your ``urls.py`` in the ``geodjango`` application folder to look
-as follows::
+Next, edit your ``urls.py`` in the ``geodjango`` application folder as follows::
from django.conf.urls import patterns, url, include
from django.contrib.gis import admin
@@ -775,9 +761,9 @@ With the :class:`~django.contrib.gis.admin.OSMGeoAdmin`, GeoDjango uses
a `Open Street Map`_ layer in the admin.
This provides more context (including street and thoroughfare details) than
available with the :class:`~django.contrib.gis.admin.GeoModelAdmin`
-(which uses the `Vector Map Level 0`_ WMS data set hosted at `OSGeo`_).
+(which uses the `Vector Map Level 0`_ WMS dataset hosted at `OSGeo`_).
-First, there are some important requirements and limitations:
+First, there are some important requirements:
* :class:`~django.contrib.gis.admin.OSMGeoAdmin` requires that the
:ref:`spherical mercator projection be added <addgoogleprojection>`
@@ -785,14 +771,19 @@ First, there are some important requirements and limitations:
* The PROJ.4 datum shifting files must be installed (see the
:ref:`PROJ.4 installation instructions <proj4>` for more details).
-If you meet these requirements, then just substitute in the ``OSMGeoAdmin``
+If you meet these requirements, then just substitute the ``OSMGeoAdmin``
option class in your ``admin.py`` file::
admin.site.register(WorldBorder, admin.OSMGeoAdmin)
.. rubric:: Footnotes
-.. [#] Special thanks to Bjørn Sandvik of `thematicmapping.org <http://thematicmapping.org>`_ for providing and maintaining this data set.
-.. [#] GeoDjango basic apps was written by Dane Springmeyer, Josh Livni, and Christopher Schmidt.
-.. [#] Here the point is for the `University of Houston Law Center <http://www.law.uh.edu/>`_.
-.. [#] Open Geospatial Consortium, Inc., `OpenGIS Simple Feature Specification For SQL <http://www.opengeospatial.org/standards/sfs>`_.
+.. [#] Special thanks to Bjørn Sandvik of `thematicmapping.org
+ <http://thematicmapping.org>`_ for providing and maintaining this
+ dataset.
+.. [#] GeoDjango basic apps was written by Dane Springmeyer, Josh Livni, and
+ Christopher Schmidt.
+.. [#] This point is the `University of Houston Law Center
+ <http://www.law.uh.edu/>`_.
+.. [#] Open Geospatial Consortium, Inc., `OpenGIS Simple Feature Specification
+ For SQL <http://www.opengeospatial.org/standards/sfs>`_.
diff --git a/docs/ref/contrib/messages.txt b/docs/ref/contrib/messages.txt
index bc921a9d33..4fa733edb5 100644
--- a/docs/ref/contrib/messages.txt
+++ b/docs/ref/contrib/messages.txt
@@ -341,7 +341,7 @@ This sets the minimum message that will be saved in the message storage. See
MESSAGE_STORAGE
---------------
-Default: ``'django.contrib.messages.storage.user_messages.FallbackStorage'``
+Default: ``'django.contrib.messages.storage.fallback.FallbackStorage'``
Controls where Django stores message data. Valid values are:
diff --git a/docs/ref/contrib/sites.txt b/docs/ref/contrib/sites.txt
index 790e003453..7e5448b3d3 100644
--- a/docs/ref/contrib/sites.txt
+++ b/docs/ref/contrib/sites.txt
@@ -127,8 +127,10 @@ For example::
def my_view(request):
if settings.SITE_ID == 3:
# Do something.
+ pass
else:
# Do something else.
+ pass
Of course, it's ugly to hard-code the site IDs like that. This sort of
hard-coding is best for hackish fixes that you need done quickly. The
@@ -141,11 +143,13 @@ domain::
current_site = get_current_site(request)
if current_site.domain == 'foo.com':
# Do something
+ pass
else:
# Do something else.
+ pass
-This has also the advantage of checking if the sites framework is installed, and
-return a :class:`RequestSite` instance if it is not.
+This has also the advantage of checking if the sites framework is installed,
+and return a :class:`RequestSite` instance if it is not.
If you don't have access to the request object, you can use the
``get_current()`` method of the :class:`~django.contrib.sites.models.Site`
@@ -158,8 +162,10 @@ the :setting:`SITE_ID` setting. This example is equivalent to the previous one::
current_site = Site.objects.get_current()
if current_site.domain == 'foo.com':
# Do something
+ pass
else:
# Do something else.
+ pass
Getting the current domain for display
--------------------------------------
@@ -200,8 +206,8 @@ subscribing to LJWorld.com alerts." Same goes for the email's message body.
Note that an even more flexible (but more heavyweight) way of doing this would
be to use Django's template system. Assuming Lawrence.com and LJWorld.com have
-different template directories (:setting:`TEMPLATE_DIRS`), you could simply farm out
-to the template system like so::
+different template directories (:setting:`TEMPLATE_DIRS`), you could simply
+farm out to the template system like so::
from django.core.mail import send_mail
from django.template import loader, Context
@@ -216,9 +222,9 @@ to the template system like so::
# ...
-In this case, you'd have to create :file:`subject.txt` and :file:`message.txt` template
-files for both the LJWorld.com and Lawrence.com template directories. That
-gives you more flexibility, but it's also more complex.
+In this case, you'd have to create :file:`subject.txt` and :file:`message.txt`
+template files for both the LJWorld.com and Lawrence.com template directories.
+That gives you more flexibility, but it's also more complex.
It's a good idea to exploit the :class:`~django.contrib.sites.models.Site`
objects as much as possible, to remove unneeded complexity and redundancy.
@@ -240,6 +246,15 @@ To do this, you can use the sites framework. A simple example::
>>> 'http://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
'http://example.com/mymodel/objects/3/'
+
+Default site and ``syncdb``
+===========================
+
+``django.contrib.sites`` registers a
+:data:`~django.db.models.signals.post_syncdb` signal handler which creates a
+default site named ``example.com`` with the domain ``example.com``. For
+example, this site will be created after Django creates the test database.
+
Caching the current ``Site`` object
===================================
diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt
index 27b8fc0875..2418dba8ef 100644
--- a/docs/ref/contrib/syndication.txt
+++ b/docs/ref/contrib/syndication.txt
@@ -53,6 +53,7 @@ This simple example, taken from `chicagocrime.org`_, describes a feed of the
latest five news items::
from django.contrib.syndication.views import Feed
+ from django.core.urlresolvers import reverse
from chicagocrime.models import NewsItem
class LatestEntriesFeed(Feed):
@@ -69,6 +70,10 @@ latest five news items::
def item_description(self, item):
return item.description
+ # item_link is only needed if NewsItem has no get_absolute_url method.
+ def item_link(self, item):
+ return reverse('news-item', args=[item.pk])
+
To connect a URL to this feed, put an instance of the Feed object in
your :doc:`URLconf </topics/http/urls>`. For example::
diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt
index 3a52f838e7..352c0f4584 100644
--- a/docs/ref/databases.txt
+++ b/docs/ref/databases.txt
@@ -33,6 +33,11 @@ aggregate with a database backend that falls within the affected release range.
.. _known to be faulty: http://archives.postgresql.org/pgsql-bugs/2007-07/msg00046.php
.. _Release 8.2.5: http://www.postgresql.org/docs/devel/static/release-8-2-5.html
+PostgreSQL connection settings
+-------------------------------
+
+See :setting:`HOST` for details.
+
Optimizing PostgreSQL's configuration
-------------------------------------
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index 7fa7539985..306db8439e 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -96,6 +96,9 @@ cleanup
Can be run as a cronjob or directly to clean out old data from the database
(only expired sessions at the moment).
+.. versionchanged:: 1.5
+ :djadmin:`cleanup` is deprecated. Use :djadmin:`clearsessions` instead.
+
compilemessages
---------------
@@ -876,14 +879,19 @@ either the path to a directory with the app template file, or a path to a
compressed file (``.tar.gz``, ``.tar.bz2``, ``.tgz``, ``.tbz``, ``.zip``)
containing the app template files.
+For example, this would look for an app template in the given directory when
+creating the ``myapp`` app::
+
+ django-admin.py startapp --template=/Users/jezdez/Code/my_app_template myapp
+
Django will also accept URLs (``http``, ``https``, ``ftp``) to compressed
archives with the app template files, downloading and extracting them on the
fly.
-For example, this would look for an app template in the given directory when
-creating the ``myapp`` app::
+For example, taking advantage of Github's feature to expose repositories as
+zip files, you can use a URL like::
- django-admin.py startapp --template=/Users/jezdez/Code/my_app_template myapp
+ django-admin.py startapp --template=https://github.com/githubuser/django-app-template/archive/master.zip myapp
.. versionadded:: 1.4
@@ -951,6 +959,15 @@ when creating the ``myproject`` project::
django-admin.py startproject --template=/Users/jezdez/Code/my_project_template myproject
+Django will also accept URLs (``http``, ``https``, ``ftp``) to compressed
+archives with the project template files, downloading and extracting them on the
+fly.
+
+For example, taking advantage of Github's feature to expose repositories as
+zip files, you can use a URL like::
+
+ django-admin.py startproject --template=https://github.com/githubuser/django-project-template/archive/master.zip myproject
+
When Django copies the project template files, it also renders certain files
through the template engine: the files whose extensions match the
``--extension`` option (``py`` by default) and the files whose names are passed
@@ -1187,6 +1204,16 @@ This command is only available if :doc:`GeoDjango </ref/contrib/gis/index>`
Please refer to its :djadmin:`description <ogrinspect>` in the GeoDjango
documentation.
+``django.contrib.sessions``
+---------------------------
+
+clearsessions
+~~~~~~~~~~~~~~~
+
+.. django-admin:: clearsessions
+
+Can be run as a cron job or directly to clean out expired sessions.
+
``django.contrib.sitemaps``
---------------------------
@@ -1456,3 +1483,12 @@ Examples::
from django.core import management
management.call_command('flush', verbosity=0, interactive=False)
management.call_command('loaddata', 'test_data', verbosity=0)
+
+Output redirection
+==================
+
+Note that you can redirect standard output and error streams as all commands
+support the ``stdout`` and ``stderr`` options. For example, you could write::
+
+ with open('/tmp/command_output') as f:
+ management.call_command('dumpdata', stdout=f)
diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt
index 7c8d509031..75d05c6829 100644
--- a/docs/ref/forms/fields.txt
+++ b/docs/ref/forms/fields.txt
@@ -683,7 +683,7 @@ For each field, we describe the default widget used if you don't specify
.. attribute:: unpack_ipv4
- Unpacks IPv4 mapped addresses like ``::ffff::192.0.2.1``.
+ Unpacks IPv4 mapped addresses like ``::ffff:192.0.2.1``.
If this option is enabled that address would be unpacked to
``192.0.2.1``. Default is disabled. Can only be used
when ``protocol`` is set to ``'both'``.
diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt
index 3c458930fa..a0ef0731ad 100644
--- a/docs/ref/forms/widgets.txt
+++ b/docs/ref/forms/widgets.txt
@@ -214,38 +214,49 @@ foundation for custom widgets.
The 'value' given is not guaranteed to be valid input, therefore
subclass implementations should program defensively.
+ .. method:: value_from_datadict(self, data, files, name)
+
+ Given a dictionary of data and this widget's name, returns the value
+ of this widget. Returns ``None`` if a value wasn't provided.
+
.. class:: MultiWidget(widgets, attrs=None)
A widget that is composed of multiple widgets.
:class:`~django.forms.widgets.MultiWidget` works hand in hand with the
:class:`~django.forms.MultiValueField`.
- .. method:: render(name, value, attrs=None)
+ :class:`MultiWidget` has one required argument:
- Argument `value` is handled differently in this method from the
- subclasses of :class:`~Widget`.
+ .. attribute:: MultiWidget.widgets
- If `value` is a list, output of :meth:`~MultiWidget.render` will be a
- concatenation of rendered child widgets. If `value` is not a list, it
- will be first processed by the method :meth:`~MultiWidget.decompress()`
- to create the list and then processed as above.
+ An iterable containing the widgets needed.
- Unlike in the single value widgets, method :meth:`~MultiWidget.render`
- need not be implemented in the subclasses.
+ And one required method:
.. method:: decompress(value)
- Returns a list of "decompressed" values for the given value of the
- multi-value field that makes use of the widget. The input value can be
- assumed as valid, but not necessarily non-empty.
+ This method takes a single "compressed" value from the field and
+ returns a list of "decompressed" values. The input value can be
+ assumed valid, but not necessarily non-empty.
This method **must be implemented** by the subclass, and since the
value may be empty, the implementation must be defensive.
The rationale behind "decompression" is that it is necessary to "split"
- the combined value of the form field into the values of the individual
- field encapsulated within the multi-value field (e.g. when displaying
- the partially or fully filled-out form).
+ the combined value of the form field into the values for each widget.
+
+ An example of this is how :class:`SplitDateTimeWidget` turns a
+ :class:`datetime` value into a list with date and time split into two
+ separate values::
+
+ class SplitDateTimeWidget(MultiWidget):
+
+ # ...
+
+ def decompress(self, value):
+ if value:
+ return [value.date(), value.time().replace(microsecond=0)]
+ return [None, None]
.. tip::
@@ -254,6 +265,109 @@ foundation for custom widgets.
with the opposite responsibility - to combine cleaned values of
all member fields into one.
+ Other methods that may be useful to override include:
+
+ .. method:: render(name, value, attrs=None)
+
+ Argument ``value`` is handled differently in this method from the
+ subclasses of :class:`~Widget` because it has to figure out how to
+ split a single value for display in multiple widgets.
+
+ The ``value`` argument used when rendering can be one of two things:
+
+ * A ``list``.
+ * A single value (e.g., a string) that is the "compressed" representation
+ of a ``list`` of values.
+
+ If `value` is a list, output of :meth:`~MultiWidget.render` will be a
+ concatenation of rendered child widgets. If `value` is not a list, it
+ will be first processed by the method :meth:`~MultiWidget.decompress()`
+ to create the list and then processed as above.
+
+ In the second case -- i.e., if the value is *not* a list --
+ ``render()`` will first decompress the value into a ``list`` before
+ rendering it. It does so by calling the ``decompress()`` method, which
+ :class:`MultiWidget`'s subclasses must implement (see above).
+
+ When ``render()`` executes its HTML rendering, each value in the list
+ is rendered with the corresponding widget -- the first value is
+ rendered in the first widget, the second value is rendered in the
+ second widget, etc.
+
+ Unlike in the single value widgets, method :meth:`~MultiWidget.render`
+ need not be implemented in the subclasses.
+
+ .. method:: format_output(rendered_widgets)
+
+ Given a list of rendered widgets (as strings), returns a Unicode string
+ representing the HTML for the whole lot.
+
+ This hook allows you to format the HTML design of the widgets any way
+ you'd like.
+
+ Here's an example widget which subclasses :class:`MultiWidget` to display
+ a date with the day, month, and year in different select boxes. This widget
+ is intended to be used with a :class:`~django.forms.DateField` rather than
+ a :class:`~django.forms.MultiValueField`, thus we have implemented
+ :meth:`~Widget.value_from_datadict`::
+
+ from datetime import date
+ from django.forms import widgets
+
+ class DateSelectorWidget(widgets.MultiWidget):
+ def __init__(self, attrs=None):
+ # create choices for days, months, years
+ # example below, the rest snipped for brevity.
+ years = [(year, year) for year in (2011, 2012, 2013)]
+ _widgets = (
+ widgets.Select(attrs=attrs, choices=days),
+ widgets.Select(attrs=attrs, choices=months),
+ widgets.Select(attrs=attrs, choices=years),
+ )
+ super(DateSelectorWidget, self).__init__(_widgets, attrs)
+
+ def decompress(self, value):
+ if value:
+ return [value.day, value.month, value.year]
+ return [None, None, None]
+
+ def format_output(self, rendered_widgets):
+ return u''.join(rendered_widgets)
+
+ def value_from_datadict(self, data, files, name):
+ datelist = [
+ widget.value_from_datadict(data, files, name + '_%s' % i)
+ for i, widget in enumerate(self.widgets)]
+ try:
+ D = date(day=int(datelist[0]), month=int(datelist[1]),
+ year=int(datelist[2]))
+ except ValueError:
+ return ''
+ else:
+ return str(D)
+
+ The constructor creates several :class:`Select` widgets in a tuple. The
+ ``super`` class uses this tuple to setup the widget.
+
+ The :meth:`~MultiWidget.format_output` method is fairly vanilla here (in
+ fact, it's the same as what's been implemented as the default for
+ ``MultiWidget``), but the idea is that you could add custom HTML between
+ the widgets should you wish.
+
+ The required method :meth:`~MultiWidget.decompress` breaks up a
+ ``datetime.date`` value into the day, month, and year values corresponding
+ to each widget. Note how the method handles the case where ``value`` is
+ ``None``.
+
+ The default implementation of :meth:`~Widget.value_from_datadict` returns
+ a list of values corresponding to each ``Widget``. This is appropriate
+ when using a ``MultiWidget`` with a :class:`~django.forms.MultiValueField`,
+ but since we want to use this widget with a :class:`~django.forms.DateField`
+ which takes a single value, we have overridden this method to combine the
+ data of all the subwidgets into a ``datetime.date``. The method extracts
+ data from the ``POST`` dictionary and constructs and validates the date.
+ If it is valid, we return the string, otherwise, we return an empty string
+ which will cause ``form.is_valid`` to return ``False``.
.. _built-in widgets:
@@ -552,54 +666,6 @@ Composite widgets
:attr:`~Field.choices` attribute. If it does, it will override anything
you set here when the attribute is updated on the :class:`Field`.
-``MultiWidget``
-~~~~~~~~~~~~~~~
-
-.. class:: MultiWidget
-
- Wrapper around multiple other widgets. You'll probably want to use this
- class with :class:`MultiValueField`.
-
- Its ``render()`` method is different than other widgets', because it has to
- figure out how to split a single value for display in multiple widgets.
-
- Subclasses may implement ``format_output``, which takes the list of
- rendered widgets and returns a string of HTML that formats them any way
- you'd like.
-
- The ``value`` argument used when rendering can be one of two things:
-
- * A ``list``.
- * A single value (e.g., a string) that is the "compressed" representation
- of a ``list`` of values.
-
- In the second case -- i.e., if the value is *not* a list -- ``render()``
- will first decompress the value into a ``list`` before rendering it. It
- does so by calling the ``decompress()`` method, which
- :class:`MultiWidget`'s subclasses must implement. This method takes a
- single "compressed" value and returns a ``list``. An example of this is how
- :class:`SplitDateTimeWidget` turns a :class:`datetime` value into a list
- with date and time split into two seperate values::
-
- class SplitDateTimeWidget(MultiWidget):
-
- # ...
-
- def decompress(self, value):
- if value:
- return [value.date(), value.time().replace(microsecond=0)]
- return [None, None]
-
- When ``render()`` executes its HTML rendering, each value in the list is
- rendered with the corresponding widget -- the first value is rendered in
- the first widget, the second value is rendered in the second widget, etc.
-
- :class:`MultiWidget` has one required argument:
-
- .. attribute:: MultiWidget.widgets
-
- An iterable containing the widgets needed.
-
``SplitDateTimeWidget``
~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index 809d56eaf5..cd1185585c 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -825,7 +825,7 @@ are converted to lowercase.
.. attribute:: GenericIPAddressField.unpack_ipv4
- Unpacks IPv4 mapped addresses like ``::ffff::192.0.2.1``.
+ Unpacks IPv4 mapped addresses like ``::ffff:192.0.2.1``.
If this option is enabled that address would be unpacked to
``192.0.2.1``. Default is disabled. Can only be used
when ``protocol`` is set to ``'both'``.
@@ -922,6 +922,11 @@ Like all :class:`CharField` subclasses, :class:`URLField` takes the optional
:attr:`~CharField.max_length`argument. If you don't specify
:attr:`~CharField.max_length`, a default of 200 is used.
+.. versionadded:: 1.5
+
+The current value of the field will be displayed as a clickable link above the
+input widget.
+
Relationship fields
===================
diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt
index 1ba41148b0..6315985ba9 100644
--- a/docs/ref/models/instances.txt
+++ b/docs/ref/models/instances.txt
@@ -67,9 +67,9 @@ Validating objects
There are three steps involved in validating a model:
-1. Validate the model fields
-2. Validate the model as a whole
-3. Validate the field uniqueness
+1. Validate the model fields - :meth:`Model.clean_fields()`
+2. Validate the model as a whole - :meth:`Model.clean()`
+3. Validate the field uniqueness - :meth:`Model.validate_unique()`
All three steps are performed when you call a model's
:meth:`~Model.full_clean()` method.
@@ -97,17 +97,20 @@ not be corrected by the user.
Note that ``full_clean()`` will *not* be called automatically when you call
your model's :meth:`~Model.save()` method, nor as a result of
-:class:`~django.forms.ModelForm` validation. You'll need to call it manually
-when you want to run one-step model validation for your own manually created
-models.
+:class:`~django.forms.ModelForm` validation. In the case of
+:class:`~django.forms.ModelForm` validation, :meth:`Model.clean_fields()`,
+:meth:`Model.clean()`, and :meth:`Model.validate_unique()` are all called
+individually.
-Example::
+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::
try:
article.full_clean()
except ValidationError as e:
# Do something based on the errors contained in e.message_dict.
# Display them to a user, or handle them programatically.
+ pass
The first step ``full_clean()`` performs is to clean each individual field.
@@ -375,7 +378,7 @@ If ``save()`` is passed a list of field names in keyword argument
``update_fields``, only the fields named in that list will be updated.
This may be desirable if you want to update just one or a few fields on
an object. There will be a slight performance benefit from preventing
-all of the model fields from being updated in the database. For example:
+all of the model fields from being updated in the database. For example::
product.name = 'Name changed again'
product.save(update_fields=['name'])
@@ -479,9 +482,13 @@ For example::
return "/people/%i/" % self.id
(Whilst this code is correct and simple, it may not be the most portable way to
-write this kind of method. The :func:`permalink() decorator <permalink>`,
-documented below, is usually the best approach and you should read that section
-before diving into code implementation.)
+write this kind of method. The :func:`~django.core.urlresolvers.reverse`
+function is usually the best approach.)
+
+For example::
+
+ def get_absolute_url(self):
+ return reverse('people.views.details', args=[str(self.id)])
One place Django uses ``get_absolute_url()`` is in the admin app. If an object
defines this method, the object-editing page will have a "View on site" link
@@ -526,11 +533,19 @@ in ``get_absolute_url()`` and have all your other code call that one place.
The ``permalink`` decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~
-The way we wrote ``get_absolute_url()`` above is a slightly violation of the
-DRY principle: the URL for this object is defined both in the URLconf file and
-in the model.
+.. warning::
-You can decouple your models from the URLconf using the ``permalink`` decorator:
+ The ``permalink`` decorator is no longer recommended. You should use
+ :func:`~django.core.urlresolvers.reverse` in the body of your
+ ``get_absolute_url`` method instead.
+
+In early versions of Django, there wasn't an easy way to use URLs defined in
+URLconf file inside :meth:`~django.db.models.Model.get_absolute_url`. That
+meant you would need to define the URL both in URLConf and
+:meth:`~django.db.models.Model.get_absolute_url`. The ``permalink`` decorator
+was added to overcome this DRY principle violation. However, since the
+introduction of :func:`~django.core.urlresolvers.reverse` there is no
+reason to use ``permalink`` any more.
.. function:: permalink()
@@ -541,14 +556,14 @@ correct URL, with all parameters substituted in the correct positions.
The ``permalink`` decorator is a Python-level equivalent to the :ttag:`url`
template tag and a high-level wrapper for the
-:func:`django.core.urlresolvers.reverse()` function.
+:func:`~django.core.urlresolvers.reverse` function.
An example should make it clear how to use ``permalink()``. Suppose your URLconf
contains a line such as::
(r'^people/(\d+)/$', 'people.views.details'),
-...your model could have a :meth:`~django.db.models.Model.get_absolute_url()`
+...your model could have a :meth:`~django.db.models.Model.get_absolute_url`
method that looked like this::
from django.db import models
@@ -616,25 +631,25 @@ the field. This method returns the "human-readable" value of the field.
For example::
- from django.db import models
+ from django.db import models
- class Person(models.Model):
- SHIRT_SIZES = (
- (u'S', u'Small'),
- (u'M', u'Medium'),
- (u'L', u'Large'),
- )
- name = models.CharField(max_length=60)
- shirt_size = models.CharField(max_length=2, choices=SHIRT_SIZES)
+ class Person(models.Model):
+ SHIRT_SIZES = (
+ (u'S', u'Small'),
+ (u'M', u'Medium'),
+ (u'L', u'Large'),
+ )
+ name = models.CharField(max_length=60)
+ shirt_size = models.CharField(max_length=2, choices=SHIRT_SIZES)
- ::
+::
- >>> p = Person(name="Fred Flintstone", shirt_size="L")
- >>> p.save()
- >>> p.shirt_size
- u'L'
- >>> p.get_shirt_size_display()
- u'Large'
+ >>> p = Person(name="Fred Flintstone", shirt_size="L")
+ >>> p.save()
+ >>> p.shirt_size
+ u'L'
+ >>> p.get_shirt_size_display()
+ u'Large'
.. method:: Model.get_next_by_FOO(\**kwargs)
.. method:: Model.get_previous_by_FOO(\**kwargs)
diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt
index c5ae8398ea..a577135271 100644
--- a/docs/ref/models/options.txt
+++ b/docs/ref/models/options.txt
@@ -261,6 +261,21 @@ Django quotes column and table names behind the scenes.
:class:`~django.db.models.ManyToManyField`, try using a signal or
an explicit :attr:`through <ManyToManyField.through>` model.
+``index_together``
+
+.. attribute:: Options.index_together
+
+ .. versionadded:: 1.5
+
+ Sets of field names that, taken together, are indexed::
+
+ index_together = [
+ ["pub_date", "deadline"],
+ ]
+
+ This list of fields will be indexed together (i.e. the appropriate
+ ``CREATE INDEX`` statement will be issued.)
+
``verbose_name``
----------------
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index 7138cd0e74..40fa2d2b2f 100644
--- a/docs/ref/models/querysets.txt
+++ b/docs/ref/models/querysets.txt
@@ -676,21 +676,12 @@ Note that, by default, ``select_related()`` does not follow foreign keys that
have ``null=True``.
Usually, using ``select_related()`` can vastly improve performance because your
-app can avoid many database calls. However, in situations with deeply nested
-sets of relationships ``select_related()`` can sometimes end up following "too
-many" relations, and can generate queries so large that they end up being slow.
+app can avoid many database calls. However, there are times you are only
+interested in specific related models, or have deeply nested sets of
+relationships, and in these cases ``select_related()`` can be optimized by
+explicitly passing the related field names you are interested in. Only
+the specified relations will be followed.
-In these situations, you can use the ``depth`` argument to ``select_related()``
-to control how many "levels" of relations ``select_related()`` will actually
-follow::
-
- b = Book.objects.select_related(depth=1).get(id=4)
- p = b.author # Doesn't hit the database.
- c = p.hometown # Requires a database call.
-
-Sometimes you only want to access specific models that are related to your root
-model, not all of the related models. In these cases, you can pass the related
-field names to ``select_related()`` and it will only follow those relations.
You can even do this for models that are more than one relation away by
separating the field names with double underscores, just as for filters. For
example, if you have this model::
@@ -730,6 +721,17 @@ You can also refer to the reverse direction of a
is defined. Instead of specifying the field name, use the :attr:`related_name
<django.db.models.ForeignKey.related_name>` for the field on the related object.
+.. deprecated:: 1.5
+ The ``depth`` parameter to ``select_related()`` has been deprecated. You
+ should replace it with the use of the ``(*fields)`` listing specific
+ related fields instead as documented above.
+
+A depth limit of relationships to follow can also be specified::
+
+ b = Book.objects.select_related(depth=1).get(id=4)
+ p = b.author # Doesn't hit the database.
+ c = p.hometown # Requires a database call.
+
A :class:`~django.db.models.OneToOneField` is not traversed in the reverse
direction if you are performing a depth-based ``select_related()`` call.
@@ -1319,10 +1321,12 @@ The above example can be rewritten using ``get_or_create()`` like so::
Any keyword arguments passed to ``get_or_create()`` — *except* an optional one
called ``defaults`` — will be used in a :meth:`get()` call. If an object is
-found, ``get_or_create()`` returns a tuple of that object and ``False``. If an
-object is *not* found, ``get_or_create()`` will instantiate and save a new
-object, returning a tuple of the new object and ``True``. The new object will
-be created roughly according to this algorithm::
+found, ``get_or_create()`` returns a tuple of that object and ``False``. If
+multiple objects are found, ``get_or_create`` raises
+:exc:`~django.core.exceptions.MultipleObjectsReturned`. If an object is *not*
+found, ``get_or_create()`` will instantiate and save a new object, returning a
+tuple of the new object and ``True``. The new object will be created roughly
+according to this algorithm::
defaults = kwargs.pop('defaults', {})
params = dict([(k, v) for k, v in kwargs.items() if '__' not in k])
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index a909c12665..daa4ee9a46 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -431,10 +431,12 @@ MySQL will connect via a Unix socket to the specified socket. For example::
If you're using MySQL and this value *doesn't* start with a forward slash, then
this value is assumed to be the host.
-If you're using PostgreSQL, an empty string means to use a Unix domain socket
-for the connection, rather than a network connection to localhost. If you
-explicitly need to use a TCP/IP connection on the local machine with
-PostgreSQL, specify ``localhost`` here.
+If you're using PostgreSQL, by default (empty :setting:`HOST`), the connection
+to the database is done through UNIX domain sockets ('local' lines in
+``pg_hba.conf``). If you want to connect through TCP sockets, set
+:setting:`HOST` to 'localhost' or '127.0.0.1' ('host' lines in ``pg_hba.conf``).
+On Windows, you should always define :setting:`HOST`, as UNIX domain sockets
+are not available.
.. setting:: NAME
@@ -1119,9 +1121,11 @@ Default: ``()``
List of compiled regular expression objects describing URLs that should be
ignored when reporting HTTP 404 errors via email (see
-:doc:`/howto/error-reporting`). Use this if your site does not provide a
-commonly requested file such as ``favicon.ico`` or ``robots.txt``, or if it
-gets hammered by script kiddies.
+:doc:`/howto/error-reporting`). Regular expressions are matched against
+:meth:`request's full paths <django.http.HttpRequest.get_full_path>` (including
+query string, if any). Use this if your site does not provide a commonly
+requested file such as ``favicon.ico`` or ``robots.txt``, or if it gets
+hammered by script kiddies.
This is only used if :setting:`SEND_BROKEN_LINK_EMAILS` is set to ``True`` and
``CommonMiddleware`` is installed (see :doc:`/topics/http/middleware`).
@@ -1242,9 +1246,8 @@ Example::
'/var/local/translations/locale'
)
-Note that in the paths you add to the value of this setting, if you have the
-typical ``/path/to/locale/xx/LC_MESSAGES`` hierarchy, you should use the path to
-the ``locale`` directory (i.e. ``'/path/to/locale'``).
+Django will look within each of these paths for the ``<locale_code>/LC_MESSAGES``
+directories containing the actual translation files.
.. setting:: LOGGING
@@ -1373,7 +1376,7 @@ more details.
MESSAGE_STORAGE
---------------
-Default: ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
+Default: ``'django.contrib.messages.storage.fallback.FallbackStorage'``
Controls where Django stores message data. See the
:doc:`messages documentation </ref/contrib/messages>` for more details.
@@ -1560,9 +1563,9 @@ for.
You'll need to set a tuple with two elements -- the name of the header to look
for and the required value. For example::
- SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
+ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
-Here, we're telling Django that we trust the ``X-Forwarded-Protocol`` header
+Here, we're telling Django that we trust the ``X-Forwarded-Proto`` header
that comes from our proxy, and any time its value is ``'https'``, then the
request is guaranteed to be secure (i.e., it originally came in via HTTPS).
Obviously, you should *only* set this setting if you control your proxy or
@@ -1575,16 +1578,18 @@ available in ``request.META``.)
.. warning::
- **You will probably open security holes in your site if you set this without knowing what you're doing. And if you fail to set it when you should. Seriously.**
+ **You will probably open security holes in your site if you set this
+ without knowing what you're doing. And if you fail to set it when you
+ should. Seriously.**
Make sure ALL of the following are true before setting this (assuming the
values from the example above):
* Your Django app is behind a proxy.
- * Your proxy strips the 'X-Forwarded-Protocol' header from all incoming
+ * Your proxy strips the ``X-Forwarded-Proto`` header from all incoming
requests. In other words, if end users include that header in their
requests, the proxy will discard it.
- * Your proxy sets the 'X-Forwarded-Protocol' header and sends it to Django,
+ * Your proxy sets the ``X-Forwarded-Proto`` header and sends it to Django,
but only for requests that originally come in via HTTPS.
If any of those are not true, you should keep this setting set to ``None``
@@ -1693,6 +1698,16 @@ This is useful if you have multiple Django instances running under the same
hostname. They can use different cookie paths, and each instance will only see
its own session cookie.
+.. setting:: SESSION_CACHE_ALIAS
+
+SESSION_CACHE_ALIAS
+-------------------
+
+Default: ``default``
+
+If you're using :ref:`cache-based session storage <cached-sessions-backend>`,
+this selects the cache to use.
+
.. setting:: SESSION_COOKIE_SECURE
SESSION_COOKIE_SECURE
@@ -2054,6 +2069,16 @@ to ensure your processes are running in the correct environment.
.. _pytz: http://pytz.sourceforge.net/
+.. setting:: TRANSACTIONS_MANAGED
+
+TRANSACTIONS_MANAGED
+--------------------
+
+Default: ``False``
+
+Set this to ``True`` if you want to :ref:`disable Django's transaction
+management <deactivate-transaction-management>` and implement your own.
+
.. setting:: USE_ETAGS
USE_ETAGS
@@ -2198,16 +2223,6 @@ The default value for the X-Frame-Options header used by
Deprecated settings
===================
-.. setting:: ADMIN_MEDIA_PREFIX
-
-ADMIN_MEDIA_PREFIX
-------------------
-
-.. deprecated:: 1.4
- This setting has been obsoleted by the ``django.contrib.staticfiles`` app
- integration. See the :doc:`Django 1.4 release notes</releases/1.4>` for
- more information.
-
.. setting:: AUTH_PROFILE_MODULE
AUTH_PROFILE_MODULE
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 3b8d058fb4..57ef0cfb27 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -53,6 +53,13 @@ comment
Ignores everything between ``{% comment %}`` and ``{% endcomment %}``.
+Sample usage::
+
+ <p>Rendered text with {{ pub_date|date:"c" }}</p>
+ {% comment %}
+ <p>Commented out text with {{ create_date|date:"c" }}</p>
+ {% endcomment %}
+
.. templatetag:: csrf_token
csrf_token
@@ -611,7 +618,7 @@ Output the contents of the block if the two arguments equal each other.
Example::
- {% ifequal user.id comment.user_id %}
+ {% ifequal user.pk comment.user_id %}
...
{% endifequal %}
@@ -947,6 +954,10 @@ Argument Outputs
``closecomment`` ``#}``
================== =======
+Sample usage::
+
+ {% templatetag openblock %} url 'entry_list' {% templatetag closeblock %}
+
.. templatetag:: url
url
@@ -1024,6 +1035,16 @@ This will follow the normal :ref:`namespaced URL resolution strategy
<topics-http-reversing-url-namespaces>`, including using any hints provided
by the context as to the current application.
+.. warning::
+
+ Don't forget to put quotes around the function path or pattern name!
+
+ .. versionchanged:: 1.5
+ The first parameter used not to be quoted, which was inconsistent with
+ other template tags. Since Django 1.5, it is evaluated according to
+ the usual rules: it can be a quoted string or a variable that will be
+ looked up in the context.
+
.. templatetag:: verbatim
verbatim
@@ -1226,7 +1247,8 @@ G Hour, 24-hour format without leading ``'0'`` to ``'23'``
h Hour, 12-hour format. ``'01'`` to ``'12'``
H Hour, 24-hour format. ``'00'`` to ``'23'``
i Minutes. ``'00'`` to ``'59'``
-I Not implemented.
+I Daylight Savings Time, whether it's ``'1'`` or ``'0'``
+ in effect or not.
j Day of the month without leading ``'1'`` to ``'31'``
zeros.
l Day of the week, textual, long. ``'Friday'``
@@ -1408,6 +1430,12 @@ applied to the result will only result in one round of escaping being done. So
it is safe to use this function even in auto-escaping environments. If you want
multiple escaping passes to be applied, use the :tfilter:`force_escape` filter.
+For example, you can apply ``escape`` to fields when :ttag:`autoescape` is off::
+
+ {% autoescape off %}
+ {{ title|escape }}
+ {% endautoescape %}
+
.. templatefilter:: escapejs
escapejs
@@ -1438,6 +1466,14 @@ For example::
If ``value`` is 123456789, the output would be ``117.7 MB``.
+.. admonition:: File sizes and SI units
+
+ Strictly speaking, ``filesizeformat`` does not conform to the International
+ System of Units which recommends using KiB, MiB, GiB, etc. when byte sizes
+ are calculated in powers of 1024 (which is the case here). Instead, Django
+ uses traditional unit names (KB, MB, GB, etc.) corresponding to names that
+ are more commonly used.
+
.. templatefilter:: first
first
@@ -1504,6 +1540,17 @@ that many decimal places. For example:
``34.26000`` ``{{ value|floatformat:3 }}`` ``34.260``
============ ============================= ==========
+Particularly useful is passing 0 (zero) as the argument which will round the
+float to the nearest integer.
+
+============ ================================ ==========
+``value`` Template Output
+============ ================================ ==========
+``34.23234`` ``{{ value|floatformat:"0" }}`` ``34``
+``34.00000`` ``{{ value|floatformat:"0" }}`` ``34``
+``39.56000`` ``{{ value|floatformat:"0" }}`` ``40``
+============ ================================ ==========
+
If the argument passed to ``floatformat`` is negative, it will round a number
to that many decimal places -- but only if there's a decimal part to be
displayed. For example:
@@ -1530,6 +1577,13 @@ string. This is useful in the rare cases where you need multiple escaping or
want to apply other filters to the escaped results. Normally, you want to use
the :tfilter:`escape` filter.
+For example, if you want to catch the ``<p>`` HTML elements created by
+the :tfilter:`linebreaks` filter::
+
+ {% autoescape off %}
+ {{ body|linebreaks|force_escape }}
+ {% endautoescape %}
+
.. templatefilter:: get_digit
get_digit
@@ -1899,9 +1953,9 @@ for documentation of Python string formatting
For example::
- {{ value|stringformat:"s" }}
+ {{ value|stringformat:"E" }}
-If ``value`` is ``"Joel is a slug"``, the output will be ``"Joel is a slug"``.
+If ``value`` is ``10``, the output will be ``1.000000E+01``.
.. templatefilter:: striptags
@@ -1967,7 +2021,9 @@ Takes an optional argument that is a variable containing the date to use as
the comparison point (without the argument, the comparison point is *now*).
For example, if ``blog_date`` is a date instance representing midnight on 1
June 2006, and ``comment_date`` is a date instance for 08:00 on 1 June 2006,
-then ``{{ blog_date|timesince:comment_date }}`` would return "8 hours".
+then the following would return "8 hours"::
+
+ {{ blog_date|timesince:comment_date }}
Comparing offset-naive and offset-aware datetimes will return an empty string.
@@ -1986,7 +2042,9 @@ given date or datetime. For example, if today is 1 June 2006 and
Takes an optional argument that is a variable containing the date to use as
the comparison point (instead of *now*). If ``from_date`` contains 22 June
-2006, then ``{{ conference_date|timeuntil:from_date }}`` will return "1 week".
+2006, then the following will return "1 week"::
+
+ {{ conference_date|timeuntil:from_date }}
Comparing offset-naive and offset-aware datetimes will return an empty string.
diff --git a/docs/ref/unicode.txt b/docs/ref/unicode.txt
index ffab647379..784ff33398 100644
--- a/docs/ref/unicode.txt
+++ b/docs/ref/unicode.txt
@@ -262,11 +262,11 @@ Taking care in ``get_absolute_url()``
URLs can only contain ASCII characters. If you're constructing a URL from
pieces of data that might be non-ASCII, be careful to encode the results in a
-way that is suitable for a URL. The ``django.db.models.permalink()`` decorator
-handles this for you automatically.
+way that is suitable for a URL. The :func:`~django.core.urlresolvers.reverse`
+function handles this for you automatically.
-If you're constructing a URL manually (i.e., *not* using the ``permalink()``
-decorator), you'll need to take care of the encoding yourself. In this case,
+If you're constructing a URL manually (i.e., *not* using the ``reverse()``
+function), you'll need to take care of the encoding yourself. In this case,
use the ``iri_to_uri()`` and ``urlquote()`` functions that were documented
above_. For example::
diff --git a/docs/ref/urlresolvers.txt b/docs/ref/urlresolvers.txt
index 1bb33c7ca1..528f172061 100644
--- a/docs/ref/urlresolvers.txt
+++ b/docs/ref/urlresolvers.txt
@@ -178,25 +178,17 @@ whether a view would raise a ``Http404`` error before redirecting to it::
return HttpResponseRedirect('/')
return response
-
-permalink()
------------
-
-The :func:`~django.db.models.permalink` decorator is useful for writing short
-methods that return a full URL path. For example, a model's
-``get_absolute_url()`` method. See :func:`django.db.models.permalink` for more.
-
get_script_prefix()
-------------------
.. function:: get_script_prefix()
-Normally, you should always use :func:`~django.core.urlresolvers.reverse` or
-:func:`~django.db.models.permalink` to define URLs within your application.
-However, if your application constructs part of the URL hierarchy itself, you
-may occasionally need to generate URLs. In that case, you need to be able to
-find the base URL of the Django project within its Web server
-(normally, :func:`~django.core.urlresolvers.reverse` takes care of this for
-you). In that case, you can call ``get_script_prefix()``, which will return the
-script prefix portion of the URL for your Django project. If your Django
-project is at the root of its Web server, this is always ``"/"``.
+Normally, you should always use :func:`~django.core.urlresolvers.reverse` to
+define URLs within your application. However, if your application constructs
+part of the URL hierarchy itself, you may occasionally need to generate URLs.
+In that case, you need to be able to find the base URL of the Django project
+within its Web server (normally, :func:`~django.core.urlresolvers.reverse`
+takes care of this for you). In that case, you can call
+``get_script_prefix()``, which will return the script prefix portion of the URL
+for your Django project. If your Django project is at the root of its web
+server, this is always ``"/"``.
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index bd3898172a..2f12c3a96c 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -305,6 +305,18 @@ The functions defined in this module share the following properties:
Returns an ASCII string containing the encoded result.
+.. function:: filepath_to_uri(path)
+
+ Convert a file system path to a URI portion that is suitable for inclusion
+ in a URL. The path is assumed to be either UTF-8 or unicode.
+
+ This method will encode certain characters that would normally be
+ recognized as special characters for URIs. Note that this method does not
+ encode the ' character, as it is a valid character within URIs. See
+ ``encodeURIComponent()`` JavaScript function for more details.
+
+ Returns an ASCII string containing the encoded result.
+
``django.utils.feedgenerator``
==============================
diff --git a/docs/releases/1.0-beta-2.txt b/docs/releases/1.0-beta-2.txt
index 288ac8fbc1..fac64d8433 100644
--- a/docs/releases/1.0-beta-2.txt
+++ b/docs/releases/1.0-beta-2.txt
@@ -32,8 +32,8 @@ Refactored ``django.contrib.comments``
carried out a major rewrite and refactoring of Django's bundled
comment system, greatly increasing its flexibility and
customizability. :doc:`Full documentation
- </ref/contrib/comments/index>` is available, as well as :doc:`an
- upgrade guide </ref/contrib/comments/upgrade>` if you were using
+ </ref/contrib/comments/index>` is available, as well as an
+ upgrade guide if you were using
the previous incarnation of the comments application..
Refactored documentation
diff --git a/docs/releases/1.0-porting-guide.txt b/docs/releases/1.0-porting-guide.txt
index a6a6bfe7d9..29e40b2ebe 100644
--- a/docs/releases/1.0-porting-guide.txt
+++ b/docs/releases/1.0-porting-guide.txt
@@ -391,8 +391,8 @@ Comments
--------
If you were using Django 0.96's ``django.contrib.comments`` app, you'll need to
-upgrade to the new comments app introduced in 1.0. See
-:doc:`/ref/contrib/comments/upgrade` for details.
+upgrade to the new comments app introduced in 1.0. See the upgrade guide
+for details.
Template tags
-------------
diff --git a/docs/releases/1.0.txt b/docs/releases/1.0.txt
index 1e44f57de8..be61311232 100644
--- a/docs/releases/1.0.txt
+++ b/docs/releases/1.0.txt
@@ -194,8 +194,8 @@ Refactored ``django.contrib.comments``
As part of a Google Summer of Code project, Thejaswi Puthraya carried out a
major rewrite and refactoring of Django's bundled comment system, greatly
increasing its flexibility and customizability. :doc:`Full documentation
-</ref/contrib/comments/index>` is available, as well as :doc:`an upgrade guide
-</ref/contrib/comments/upgrade>` if you were using the previous incarnation of
+</ref/contrib/comments/index>` is available, as well as an upgrade guide
+if you were using the previous incarnation of
the comments application.
Removal of deprecated features
diff --git a/docs/releases/1.5-alpha-1.txt b/docs/releases/1.5-alpha-1.txt
index 8f027c6859..8fbeafc68b 100644
--- a/docs/releases/1.5-alpha-1.txt
+++ b/docs/releases/1.5-alpha-1.txt
@@ -51,8 +51,8 @@ Other notable new features in Django 1.5 include:
* `Support for saving a subset of model's fields`_ -
:meth:`Model.save() <django.db.models.Model.save()>` now accepts an
``update_fields`` argument, letting you specify which fields are
- written back to the databse when you call ``save()``. This can help
- in high-concurrancy operations, and can improve performance.
+ written back to the database when you call ``save()``. This can help
+ in high-concurrency operations, and can improve performance.
* Better `support for streaming responses <#explicit-streaming-responses>`_ via
the new :class:`~django.http.StreamingHttpResponse` response class.
@@ -83,7 +83,7 @@ Python compatibility
====================
Django 1.5 requires Python 2.6.5 or above, though we **highly recommended**
-Python 2.7.3 or above. Support for Python 2.5 and below as been dropped.
+Python 2.7.3 or above. Support for Python 2.5 and below has been dropped.
This change should affect only a small number of Django users, as most
operating-system vendors today are shipping Python 2.6 or newer as their default
@@ -106,17 +106,17 @@ need to install a different version of Django on Python 3. This means that
you can write application targeted for just Python 2, just Python 3, or single
applications that support both platforms.
-However, we're labling this support "experimental" for now: although it's
-receved extensive testing via our automated test suite, it's recieved very
+However, we're labeling this support "experimental" for now: although it's
+received extensive testing via our automated test suite, it's received very
little real-world testing. We've done our best to eliminate bugs, but we can't
be sure we covered all possible uses of Django. Further, Django's more than a
web framework; it's an ecosystem of pluggable components. At this point, very
-few third-party applications have been ported to Python 3, so it's unliukely
-that a real-world application will have all its dependecies satisfied under
+few third-party applications have been ported to Python 3, so it's unlikely
+that a real-world application will have all its dependencies satisfied under
Python 3.
Thus, we're recommending that Django 1.5 not be used in production under Python
-3. Instead, use this oportunity to begin :doc:`porting applications to Python 3
+3. Instead, use this opportunity to begin :doc:`porting applications to Python 3
</topics/python3>`. If you're an author of a pluggable component, we encourage you
to start porting now.
@@ -207,7 +207,7 @@ Retrieval of ``ContentType`` instances associated with proxy models
The methods :meth:`ContentTypeManager.get_for_model() <django.contrib.contenttypes.models.ContentTypeManager.get_for_model()>`
and :meth:`ContentTypeManager.get_for_models() <django.contrib.contenttypes.models.ContentTypeManager.get_for_models()>`
have a new keyword argument – respectively ``for_concrete_model`` and ``for_concrete_models``.
-By passing ``False`` using this argument it is now possible to retreive the
+By passing ``False`` using this argument it is now possible to retrieve the
:class:`ContentType <django.contrib.contenttypes.models.ContentType>`
associated with proxy models.
@@ -495,8 +495,8 @@ In order to be able to run unit tests in any order and to make sure they are
always isolated from each other, :class:`~django.test.TransactionTestCase` will
now reset the database *after* each test run instead.
-No more implict DB sequences reset
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+No more implicit DB sequences reset
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
:class:`~django.test.TransactionTestCase` tests used to reset primary key
sequences automatically together with the database flushing actions described
@@ -554,7 +554,7 @@ Miscellaneous
available at :func:`django.utils.html.remove_tags`.
* Uploaded files are no longer created as executable by default. If you need
- them to be executeable change :setting:`FILE_UPLOAD_PERMISSIONS` to your
+ them to be executable change :setting:`FILE_UPLOAD_PERMISSIONS` to your
needs. The new default value is `0666` (octal) and the current umask value
is first masked out.
diff --git a/docs/releases/1.5-beta-1.txt b/docs/releases/1.5-beta-1.txt
new file mode 100644
index 0000000000..f3bfc2a8fa
--- /dev/null
+++ b/docs/releases/1.5-beta-1.txt
@@ -0,0 +1,702 @@
+=============================
+Django 1.5 beta release notes
+=============================
+
+November 27, 2012.
+
+Welcome to Django 1.5 beta!
+
+This is the second in a series of preview/development releases leading
+up to the eventual release of Django 1.5, scheduled for Decemeber
+2012. This release is primarily targeted at developers who are
+interested in trying out new features and testing the Django codebase
+to help identify and resolve bugs prior to the final 1.5 release.
+
+As such, this release is *not* intended for production use, and any such use
+is discouraged.
+
+These release notes cover the `new features`_, as well
+as some `backwards incompatible changes`_ you'll want to be aware of
+when upgrading from Django 1.4 or older versions. We've also dropped some
+features, which are detailed in :doc:`our deprecation plan
+</internals/deprecation>`, and we've `begun the deprecation process for some
+features`_.
+
+.. _`new features`: `What's new in Django 1.5`_
+.. _`backwards incompatible changes`: `Backwards incompatible changes in 1.5`_
+.. _`begun the deprecation process for some features`: `Features deprecated in 1.5`_
+
+Overview
+========
+
+The biggest new feature in Django 1.5 is the `configurable User model`_. Before
+Django 1.5, applications that wanted to use Django's auth framework
+(:mod:`django.contrib.auth`) were forced to use Django's definition of a "user".
+In Django 1.5, you can now swap out the ``User`` model for one that you write
+yourself. This could be a simple extension to the existing ``User`` model -- for
+example, you could add a Twitter or Facebook ID field -- or you could completely
+replace the ``User`` with one totally customized for your site.
+
+Django 1.5 is also the first release with `Python 3 support`_! We're labeling
+this support "experimental" because we don't yet consider it production-ready,
+but everything's in place for you to start porting your apps to Python 3.
+Our next release, Django 1.6, will support Python 3 without reservations.
+
+Other notable new features in Django 1.5 include:
+
+* `Support for saving a subset of model's fields`_ -
+ :meth:`Model.save() <django.db.models.Model.save()>` now accepts an
+ ``update_fields`` argument, letting you specify which fields are
+ written back to the database when you call ``save()``. This can help
+ in high-concurrency operations, and can improve performance.
+
+* Better `support for streaming responses <#explicit-streaming-responses-beta-1>`_ via
+ the new :class:`~django.http.StreamingHttpResponse` response class.
+
+* `GeoDjango`_ now supports PostGIS 2.0.
+
+* ... and more; `see below <#what-s-new-in-django-1-5>`_.
+
+Wherever possible we try to introduce new features in a backwards-compatible
+manner per :doc:`our API stability policy </misc/api-stability>`.
+However, as with previous releases, Django 1.5 ships with some minor
+`backwards incompatible changes`_; people upgrading from previous versions
+of Django should read that list carefully.
+
+One deprecated feature worth noting is the shift to "new-style" :ttag:`url` tag.
+Prior to Django 1.3, syntax like ``{% url myview %}`` was interpreted
+incorrectly (Django considered ``"myview"`` to be a literal name of a view, not
+a template variable named ``myview``). Django 1.3 and above introduced the
+``{% load url from future %}`` syntax to bring in the corrected behavior where
+``myview`` was seen as a variable.
+
+The upshot of this is that if you are not using ``{% load url from future %}``
+in your templates, you'll need to change tags like ``{% url myview %}`` to
+``{% url "myview" %}``. If you *were* using ``{% load url from future %}`` you
+can simply remove that line under Django 1.5
+
+Python compatibility
+====================
+
+Django 1.5 requires Python 2.6.5 or above, though we **highly recommend**
+Python 2.7.3 or above. Support for Python 2.5 and below has been dropped.
+
+This change should affect only a small number of Django users, as most
+operating-system vendors today are shipping Python 2.6 or newer as their default
+version. If you're still using Python 2.5, however, you'll need to stick to
+Django 1.4 until you can upgrade your Python version. Per :doc:`our support
+policy </internals/release-process>`, Django 1.4 will continue to receive
+security support until the release of Django 1.6.
+
+Django 1.5 does not run on a Jython final release, because Jython's latest
+release doesn't currently support Python 2.6. However, Jython currently does
+offer an alpha release featuring 2.7 support, and Django 1.5 supports that alpha
+release.
+
+Python 3 support
+~~~~~~~~~~~~~~~~
+
+Django 1.5 introduces support for Python 3 - specifically, Python
+3.2 and above. This comes in the form of a **single** codebase; you don't
+need to install a different version of Django on Python 3. This means that
+you can write applications targeted for just Python 2, just Python 3, or single
+applications that support both platforms.
+
+However, we're labeling this support "experimental" for now: although it's
+received extensive testing via our automated test suite, it's received very
+little real-world testing. We've done our best to eliminate bugs, but we can't
+be sure we covered all possible uses of Django. Further, Django's more than a
+web framework; it's an ecosystem of pluggable components. At this point, very
+few third-party applications have been ported to Python 3, so it's unlikely
+that a real-world application will have all its dependencies satisfied under
+Python 3.
+
+Thus, we're recommending that Django 1.5 not be used in production under Python
+3. Instead, use this opportunity to begin :doc:`porting applications to Python 3
+</topics/python3>`. If you're an author of a pluggable component, we encourage you
+to start porting now.
+
+We plan to offer first-class, production-ready support for Python 3 in our next
+release, Django 1.6.
+
+What's new in Django 1.5
+========================
+
+Configurable User model
+~~~~~~~~~~~~~~~~~~~~~~~
+
+In Django 1.5, you can now use your own model as the store for user-related
+data. If your project needs a username with more than 30 characters, or if
+you want to store user's names in a format other than first name/last name,
+or you want to put custom profile information onto your User object, you can
+now do so.
+
+If you have a third-party reusable application that references the User model,
+you may need to make some changes to the way you reference User instances. You
+should also document any specific features of the User model that your
+application relies upon.
+
+See the :ref:`documentation on custom User models <auth-custom-user>` for
+more details.
+
+Support for saving a subset of model's fields
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The method :meth:`Model.save() <django.db.models.Model.save()>` has a new
+keyword argument ``update_fields``. By using this argument it is possible to
+save only a select list of model's fields. This can be useful for performance
+reasons or when trying to avoid overwriting concurrent changes.
+
+Deferred instances (those loaded by .only() or .defer()) will automatically
+save just the loaded fields. If any field is set manually after load, that
+field will also get updated on save.
+
+See the :meth:`Model.save() <django.db.models.Model.save()>` documentation for
+more details.
+
+Caching of related model instances
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When traversing relations, the ORM will avoid re-fetching objects that were
+previously loaded. For example, with the tutorial's models::
+
+ >>> first_poll = Poll.objects.all()[0]
+ >>> first_choice = first_poll.choice_set.all()[0]
+ >>> first_choice.poll is first_poll
+ True
+
+In Django 1.5, the third line no longer triggers a new SQL query to fetch
+``first_choice.poll``; it was set by the second line.
+
+For one-to-one relationships, both sides can be cached. For many-to-one
+relationships, only the single side of the relationship can be cached. This
+is particularly helpful in combination with ``prefetch_related``.
+
+.. _explicit-streaming-responses-beta-1:
+
+Explicit support for streaming responses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Before Django 1.5, it was possible to create a streaming response by passing
+an iterator to :class:`~django.http.HttpResponse`. But this was unreliable:
+any middleware that accessed the :attr:`~django.http.HttpResponse.content`
+attribute would consume the iterator prematurely.
+
+You can now explicitly generate a streaming response with the new
+:class:`~django.http.StreamingHttpResponse` class. This class exposes a
+:class:`~django.http.StreamingHttpResponse.streaming_content` attribute which
+is an iterator.
+
+Since :class:`~django.http.StreamingHttpResponse` does not have a ``content``
+attribute, middleware that needs access to the response content must test for
+streaming responses and behave accordingly. See :ref:`response-middleware` for
+more information.
+
+``{% verbatim %}`` template tag
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To make it easier to deal with javascript templates which collide with Django's
+syntax, you can now use the :ttag:`verbatim` block tag to avoid parsing the
+tag's content.
+
+Retrieval of ``ContentType`` instances associated with proxy models
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The methods :meth:`ContentTypeManager.get_for_model() <django.contrib.contenttypes.models.ContentTypeManager.get_for_model()>`
+and :meth:`ContentTypeManager.get_for_models() <django.contrib.contenttypes.models.ContentTypeManager.get_for_models()>`
+have a new keyword argument – respectively ``for_concrete_model`` and ``for_concrete_models``.
+By passing ``False`` using this argument it is now possible to retrieve the
+:class:`ContentType <django.contrib.contenttypes.models.ContentType>`
+associated with proxy models.
+
+New ``view`` variable in class-based views context
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In all :doc:`generic class-based views </topics/class-based-views/index>`
+(or any class-based view inheriting from ``ContextMixin``), the context dictionary
+contains a ``view`` variable that points to the ``View`` instance.
+
+GeoDjango
+~~~~~~~~~
+
+* :class:`~django.contrib.gis.geos.LineString` and
+ :class:`~django.contrib.gis.geos.MultiLineString` GEOS objects now support the
+ :meth:`~django.contrib.gis.geos.GEOSGeometry.interpolate()` and
+ :meth:`~django.contrib.gis.geos.GEOSGeometry.project()` methods
+ (so-called linear referencing).
+
+* The wkb and hex properties of `GEOSGeometry` objects preserve the Z dimension.
+
+* Support for PostGIS 2.0 has been added and support for GDAL < 1.5 has been
+ dropped.
+
+Minor features
+~~~~~~~~~~~~~~
+
+Django 1.5 also includes several smaller improvements worth noting:
+
+* The template engine now interprets ``True``, ``False`` and ``None`` as the
+ corresponding Python objects.
+
+* :mod:`django.utils.timezone` provides a helper for converting aware
+ datetimes between time zones. See :func:`~django.utils.timezone.localtime`.
+
+* The generic views support OPTIONS requests.
+
+* Management commands do not raise ``SystemExit`` any more when called by code
+ from :ref:`call_command <call-command>`. Any exception raised by the command
+ (mostly :ref:`CommandError <ref-command-exceptions>`) is propagated.
+
+* The dumpdata management command outputs one row at a time, preventing
+ out-of-memory errors when dumping large datasets.
+
+* In the localflavor for Canada, "pq" was added to the acceptable codes for
+ Quebec. It's an old abbreviation.
+
+* The :ref:`receiver <connecting-receiver-functions>` decorator is now able to
+ connect to more than one signal by supplying a list of signals.
+
+* In the admin, you can now filter users by groups which they are members of.
+
+* :meth:`QuerySet.bulk_create()
+ <django.db.models.query.QuerySet.bulk_create>` now has a batch_size
+ argument. By default the batch_size is unlimited except for SQLite where
+ single batch is limited so that 999 parameters per query isn't exceeded.
+
+* The :setting:`LOGIN_URL` and :setting:`LOGIN_REDIRECT_URL` settings now also
+ accept view function names and
+ :ref:`named URL patterns <naming-url-patterns>`. This allows you to reduce
+ configuration duplication. More information can be found in the
+ :func:`~django.contrib.auth.decorators.login_required` documentation.
+
+* Django now provides a mod_wsgi :doc:`auth handler
+ </howto/deployment/wsgi/apache-auth>`.
+
+* The :meth:`QuerySet.delete() <django.db.models.query.QuerySet.delete>`
+ and :meth:`Model.delete() <django.db.models.Model.delete()>` can now take
+ fast-path in some cases. The fast-path allows for less queries and less
+ objects fetched into memory. See :meth:`QuerySet.delete()
+ <django.db.models.query.QuerySet.delete>` for details.
+
+* An instance of :class:`~django.core.urlresolvers.ResolverMatch` is stored on
+ the request as ``resolver_match``.
+
+* By default, all logging messages reaching the `django` logger when
+ :setting:`DEBUG` is `True` are sent to the console (unless you redefine the
+ logger in your :setting:`LOGGING` setting).
+
+* When using :class:`~django.template.RequestContext`, it is now possible to
+ look up permissions by using ``{% if 'someapp.someperm' in perms %}``
+ in templates.
+
+* It's not required any more to have ``404.html`` and ``500.html`` templates in
+ the root templates directory. Django will output some basic error messages for
+ both situations when those templates are not found. Of course, it's still
+ recommended as good practice to provide those templates in order to present
+ pretty error pages to the user.
+
+* :mod:`django.contrib.auth` provides a new signal that is emitted
+ whenever a user fails to login successfully. See
+ :data:`~django.contrib.auth.signals.user_login_failed`
+
+* The loaddata management command now supports an `ignorenonexistent` option to
+ ignore data for fields that no longer exist.
+
+* :meth:`~django.test.SimpleTestCase.assertXMLEqual` and
+ :meth:`~django.test.SimpleTestCase.assertXMLNotEqual` new assertions allow
+ you to test equality for XML content at a semantic level, without caring for
+ syntax differences (spaces, attribute order, etc.).
+
+* RemoteUserMiddleware now forces logout when the REMOTE_USER header
+ disappears during the same browser session.
+
+* The :ref:`cache-based session backend <cached-sessions-backend>` can store
+ session data in a non-default cache.
+
+* Multi-column indexes can now be created on models. Read the
+ :attr:`~django.db.models.Options.index_together` documentation for more
+ information.
+
+* During Django's logging configuration verbose Deprecation warnings are
+ enabled and warnings are captured into the logging system. Logged warnings
+ are routed through the ``console`` logging handler, which by default requires
+ :setting:`DEBUG` to be True for output to be generated. The result is that
+ DeprecationWarnings should be printed to the console in development
+ environments the way they have been in Python versions < 2.7.
+
+* The API for :meth:`django.contrib.admin.ModelAdmin.message_user` method has
+ been modified to accept additional arguments adding capabilities similar to
+ :func:`django.contrib.messages.add_message`. This is useful for generating
+ error messages from admin actions.
+
+* The admin's list filters can now be customized per-request thanks to the new
+ :meth:`django.contrib.admin.ModelAdmin.get_list_filter` method.
+
+Backwards incompatible changes in 1.5
+=====================================
+
+.. warning::
+
+ In addition to the changes outlined in this section, be sure to review the
+ :doc:`deprecation plan </internals/deprecation>` for any features that
+ have been removed. If you haven't updated your code within the
+ deprecation timeline for a given feature, its removal may appear as a
+ backwards incompatible change.
+
+Context in year archive class-based views
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For consistency with the other date-based generic views,
+:class:`~django.views.generic.dates.YearArchiveView` now passes ``year`` in
+the context as a :class:`datetime.date` rather than a string. If you are
+using ``{{ year }}`` in your templates, you must replace it with ``{{
+year|date:"Y" }}``.
+
+``next_year`` and ``previous_year`` were also added in the context. They are
+calculated according to ``allow_empty`` and ``allow_future``.
+
+Context in year and month archive class-based views
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+:class:`~django.views.generic.dates.YearArchiveView` and
+:class:`~django.views.generic.dates.MonthArchiveView` were documented to
+provide a ``date_list`` sorted in ascending order in the context, like their
+function-based predecessors, but it actually was in descending order. In 1.5,
+the documented order was restored. You may want to add (or remove) the
+``reversed`` keyword when you're iterating on ``date_list`` in a template::
+
+ {% for date in date_list reversed %}
+
+:class:`~django.views.generic.dates.ArchiveIndexView` still provides a
+``date_list`` in descending order.
+
+Context in TemplateView
+~~~~~~~~~~~~~~~~~~~~~~~
+
+For consistency with the design of the other generic views,
+:class:`~django.views.generic.base.TemplateView` no longer passes a ``params``
+dictionary into the context, instead passing the variables from the URLconf
+directly into the context.
+
+Non-form data in HTTP requests
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+:attr:`request.POST <django.http.HttpRequest.POST>` will no longer include data
+posted via HTTP requests with non form-specific content-types in the header.
+In prior versions, data posted with content-types other than
+``multipart/form-data`` or ``application/x-www-form-urlencoded`` would still
+end up represented in the :attr:`request.POST <django.http.HttpRequest.POST>`
+attribute. Developers wishing to access the raw POST data for these cases,
+should use the :attr:`request.body <django.http.HttpRequest.body>` attribute
+instead.
+
+OPTIONS, PUT and DELETE requests in the test client
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Unlike GET and POST, these HTTP methods aren't implemented by web browsers.
+Rather, they're used in APIs, which transfer data in various formats such as
+JSON or XML. Since such requests may contain arbitrary data, Django doesn't
+attempt to decode their body.
+
+However, the test client used to build a query string for OPTIONS and DELETE
+requests like for GET, and a request body for PUT requests like for POST. This
+encoding was arbitrary and inconsistent with Django's behavior when it
+receives the requests, so it was removed in Django 1.5.
+
+If you were using the ``data`` parameter in an OPTIONS or a DELETE request,
+you must convert it to a query string and append it to the ``path`` parameter.
+
+If you were using the ``data`` parameter in a PUT request without a
+``content_type``, you must encode your data before passing it to the test
+client and set the ``content_type`` argument.
+
+.. _simplejson-incompatibilities-beta-1:
+
+System version of :mod:`simplejson` no longer used
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+:ref:`As explained below <simplejson-deprecation-beta-1>`, Django 1.5 deprecates
+:mod:`django.utils.simplejson` in favor of Python 2.6's built-in :mod:`json`
+module. In theory, this change is harmless. Unfortunately, because of
+incompatibilities between versions of :mod:`simplejson`, it may trigger errors
+in some circumstances.
+
+JSON-related features in Django 1.4 always used :mod:`django.utils.simplejson`.
+This module was actually:
+
+- A system version of :mod:`simplejson`, if one was available (ie. ``import
+ simplejson`` works), if it was more recent than Django's built-in copy or it
+ had the C speedups, or
+- The :mod:`json` module from the standard library, if it was available (ie.
+ Python 2.6 or greater), or
+- A built-in copy of version 2.0.7 of :mod:`simplejson`.
+
+In Django 1.5, those features use Python's :mod:`json` module, which is based
+on version 2.0.9 of :mod:`simplejson`.
+
+There are no known incompatibilities between Django's copy of version 2.0.7 and
+Python's copy of version 2.0.9. However, there are some incompatibilities
+between other versions of :mod:`simplejson`:
+
+- While the :mod:`simplejson` API is documented as always returning unicode
+ strings, the optional C implementation can return a byte string. This was
+ fixed in Python 2.7.
+- :class:`simplejson.JSONEncoder` gained a ``namedtuple_as_object`` keyword
+ argument in version 2.2.
+
+More information on these incompatibilities is available in `ticket #18023`_.
+
+The net result is that, if you have installed :mod:`simplejson` and your code
+uses Django's serialization internals directly -- for instance
+:class:`django.core.serializers.json.DjangoJSONEncoder`, the switch from
+:mod:`simplejson` to :mod:`json` could break your code. (In general, changes to
+internals aren't documented; we're making an exception here.)
+
+At this point, the maintainers of Django believe that using :mod:`json` from
+the standard library offers the strongest guarantee of backwards-compatibility.
+They recommend to use it from now on.
+
+.. _ticket #18023: https://code.djangoproject.com/ticket/18023#comment:10
+
+String types of hasher method parameters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you have written a :ref:`custom password hasher <auth_password_storage>`,
+your ``encode()``, ``verify()`` or ``safe_summary()`` methods should accept
+Unicode parameters (``password``, ``salt`` or ``encoded``). If any of the
+hashing methods need byte strings, you can use the
+:func:`~django.utils.encoding.force_bytes` utility to encode the strings.
+
+Validation of previous_page_number and next_page_number
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When using :doc:`object pagination </topics/pagination>`,
+the ``previous_page_number()`` and ``next_page_number()`` methods of the
+:class:`~django.core.paginator.Page` object did not check if the returned
+number was inside the existing page range.
+It does check it now and raises an :exc:`InvalidPage` exception when the number
+is either too low or too high.
+
+Behavior of autocommit database option on PostgreSQL changed
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+PostgreSQL's autocommit option didn't work as advertised previously. It did
+work for single transaction block, but after the first block was left the
+autocommit behavior was never restored. This bug is now fixed in 1.5. While
+this is only a bug fix, it is worth checking your applications behavior if
+you are using PostgreSQL together with the autocommit option.
+
+Session not saved on 500 responses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Django's session middleware will skip saving the session data if the
+response's status code is 500.
+
+Email checks on failed admin login
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Prior to Django 1.5, if you attempted to log into the admin interface and
+mistakenly used your email address instead of your username, the admin
+interface would provide a warning advising that your email address was
+not your username. In Django 1.5, the introduction of
+:ref:`custom User models <auth-custom-user>` has required the removal of this
+warning. This doesn't change the login behavior of the admin site; it only
+affects the warning message that is displayed under one particular mode of
+login failure.
+
+Changes in tests execution
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Some changes have been introduced in the execution of tests that might be
+backward-incompatible for some testing setups:
+
+Database flushing in ``django.test.TransactionTestCase``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Previously, the test database was truncated *before* each test run in a
+:class:`~django.test.TransactionTestCase`.
+
+In order to be able to run unit tests in any order and to make sure they are
+always isolated from each other, :class:`~django.test.TransactionTestCase` will
+now reset the database *after* each test run instead.
+
+No more implicit DB sequences reset
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+:class:`~django.test.TransactionTestCase` tests used to reset primary key
+sequences automatically together with the database flushing actions described
+above.
+
+This has been changed so no sequences are implicitly reset. This can cause
+:class:`~django.test.TransactionTestCase` tests that depend on hard-coded
+primary key values to break.
+
+The new :attr:`~django.test.TransactionTestCase.reset_sequences` attribute can
+be used to force the old behavior for :class:`~django.test.TransactionTestCase`
+that might need it.
+
+Ordering of tests
+^^^^^^^^^^^^^^^^^
+
+In order to make sure all ``TestCase`` code starts with a clean database,
+tests are now executed in the following order:
+
+* First, all unittests (including :class:`unittest.TestCase`,
+ :class:`~django.test.SimpleTestCase`, :class:`~django.test.TestCase` and
+ :class:`~django.test.TransactionTestCase`) are run with no particular ordering
+ guaranteed nor enforced among them.
+
+* Then any other tests (e.g. doctests) that may alter the database without
+ restoring it to its original state are run.
+
+This should not cause any problems unless you have existing doctests which
+assume a :class:`~django.test.TransactionTestCase` executed earlier left some
+database state behind or unit tests that rely on some form of state being
+preserved after the execution of other tests. Such tests are already very
+fragile, and must now be changed to be able to run independently.
+
+`cleaned_data` dictionary kept for invalid forms
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :attr:`~django.forms.Form.cleaned_data` dictionary is now always present
+after form validation. When the form doesn't validate, it contains only the
+fields that passed validation. You should test the success of the validation
+with the :meth:`~django.forms.Form.is_valid()` method and not with the
+presence or absence of the :attr:`~django.forms.Form.cleaned_data` attribute
+on the form.
+
+Behavior of :djadmin:`syncdb` with multiple databases
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+:djadmin:`syncdb` now queries the database routers to determine if content
+types (when :mod:`~django.contrib.contenttypes` is enabled) and permissions
+(when :mod:`~django.contrib.auth` is enabled) should be created in the target
+database. Previously, it created them in the default database, even when
+another database was specified with the :djadminopt:`--database` option.
+
+If you use :djadmin:`syncdb` on multiple databases, you should ensure that
+your routers allow synchronizing content types and permissions to only one of
+them. See the docs on the :ref:`behavior of contrib apps with multiple
+databases <contrib_app_multiple_databases>` for more information.
+
+Miscellaneous
+~~~~~~~~~~~~~
+
+* :class:`django.forms.ModelMultipleChoiceField` now returns an empty
+ ``QuerySet`` as the empty value instead of an empty list.
+
+* :func:`~django.utils.http.int_to_base36` properly raises a :exc:`TypeError`
+ instead of :exc:`ValueError` for non-integer inputs.
+
+* The ``slugify`` template filter is now available as a standard python
+ function at :func:`django.utils.text.slugify`. Similarly, ``remove_tags`` is
+ available at :func:`django.utils.html.remove_tags`.
+
+* Uploaded files are no longer created as executable by default. If you need
+ them to be executable change :setting:`FILE_UPLOAD_PERMISSIONS` to your
+ needs. The new default value is `0666` (octal) and the current umask value
+ is first masked out.
+
+* The :ref:`F() expressions <query-expressions>` supported bitwise operators by
+ ``&`` and ``|``. These operators are now available using ``.bitand()`` and
+ ``.bitor()`` instead. The removal of ``&`` and ``|`` was done to be consistent with
+ :ref:`Q() expressions <complex-lookups-with-q>` and ``QuerySet`` combining where
+ the operators are used as boolean AND and OR operators.
+
+* In a ``filter()`` call, when :ref:`F() expressions <query-expressions>`
+ contained lookups spanning multi-valued relations, they didn't always reuse
+ the same relations as other lookups along the same chain. This was changed,
+ and now F() expressions will always use the same relations as other lookups
+ within the same ``filter()`` call.
+
+* The :ttag:`csrf_token` template tag is no longer enclosed in a div. If you need
+ HTML validation against pre-HTML5 Strict DTDs, you should add a div around it
+ in your pages.
+
+* The template tags library ``adminmedia``, which only contained the
+ deprecated template tag ``{% admin_media_prefix %}``, was removed.
+ Attempting to load it with ``{% load adminmedia %}`` will fail. If your
+ templates still contain that line you must remove it.
+
+Features deprecated in 1.5
+==========================
+
+.. _simplejson-deprecation-beta-1:
+
+:setting:`AUTH_PROFILE_MODULE`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+With the introduction of :ref:`custom User models <auth-custom-user>`, there is
+no longer any need for a built-in mechanism to store user profile data.
+
+You can still define user profiles models that have a one-to-one relation with
+the User model - in fact, for many applications needing to associate data with
+a User account, this will be an appropriate design pattern to follow. However,
+the :setting:`AUTH_PROFILE_MODULE` setting, and the
+:meth:`~django.contrib.auth.models.User.get_profile()` method for accessing
+the user profile model, should not be used any longer.
+
+Streaming behavior of :class:`HttpResponse`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Django 1.5 deprecates the ability to stream a response by passing an iterator
+to :class:`~django.http.HttpResponse`. If you rely on this behavior, switch to
+:class:`~django.http.StreamingHttpResponse`. See
+:ref:`explicit-streaming-responses-beta-1` above.
+
+In Django 1.7 and above, the iterator will be consumed immediately by
+:class:`~django.http.HttpResponse`.
+
+``django.utils.simplejson``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Since Django 1.5 drops support for Python 2.5, we can now rely on the
+:mod:`json` module being available in Python's standard library, so we've
+removed our own copy of :mod:`simplejson`. You should now import :mod:`json`
+instead :mod:`django.utils.simplejson`.
+
+Unfortunately, this change might have unwanted side-effects, because of
+incompatibilities between versions of :mod:`simplejson` -- see the
+:ref:`backwards-incompatible changes <simplejson-incompatibilities-beta-1>` section.
+If you rely on features added to :mod:`simplejson` after it became Python's
+:mod:`json`, you should import :mod:`simplejson` explicitly.
+
+``django.utils.encoding.StrAndUnicode``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :class:`~django.utils.encoding.StrAndUnicode` mix-in has been deprecated.
+Define a ``__str__`` method and apply the
+:func:`~django.utils.encoding.python_2_unicode_compatible` decorator instead.
+
+``django.utils.itercompat.product``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :func:`~django.utils.itercompat.product` function has been deprecated. Use
+the built-in :func:`itertools.product` instead.
+
+``django.utils.markup``
+~~~~~~~~~~~~~~~~~~~~~~~
+
+The markup contrib module has been deprecated and will follow an accelerated
+deprecation schedule. Direct use of python markup libraries or 3rd party tag
+libraries is preferred to Django maintaining this functionality in the
+framework.
+
+``cleanup`` management command
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :djadmin:`cleanup` management command has been deprecated and replaced by
+:djadmin:`clearsessions`.
+
+``daily_cleanup.py`` script
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The undocumented ``daily_cleanup.py`` script has been deprecated. Use the
+:djadmin:`clearsessions` management command instead.
+
+``depth`` keyword argument in ``select_related``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``depth`` keyword argument in
+:meth:`~django.db.models.query.QuerySet.select_related` has been deprecated.
+You should use field names instead.
diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt
index a0ce3cc7a4..6ac5120617 100644
--- a/docs/releases/1.5.txt
+++ b/docs/releases/1.5.txt
@@ -47,7 +47,7 @@ Other notable new features in Django 1.5 include:
* ... and more; `see below <#what-s-new-in-django-1-5>`_.
Wherever possible we try to introduce new features in a backwards-compatible
-manner per :doc:`our API stability policy </misc/api-stability>` policy.
+manner per :doc:`our API stability policy </misc/api-stability>`.
However, as with previous releases, Django 1.5 ships with some minor
`backwards incompatible changes`_; people upgrading from previous versions
of Django should read that list carefully.
@@ -68,7 +68,7 @@ Python compatibility
====================
Django 1.5 requires Python 2.6.5 or above, though we **highly recommended**
-Python 2.7.3 or above. Support for Python 2.5 and below as been dropped.
+Python 2.7.3 or above. Support for Python 2.5 and below has been dropped.
This change should affect only a small number of Django users, as most
operating-system vendors today are shipping Python 2.6 or newer as their default
@@ -88,7 +88,7 @@ Python 3 support
Django 1.5 introduces support for Python 3 - specifically, Python
3.2 and above. This comes in the form of a **single** codebase; you don't
need to install a different version of Django on Python 3. This means that
-you can write application targeted for just Python 2, just Python 3, or single
+you can write applications targeted for just Python 2, just Python 3, or single
applications that support both platforms.
However, we're labeling this support "experimental" for now: although it's
@@ -219,6 +219,15 @@ GeoDjango
* Support for PostGIS 2.0 has been added and support for GDAL < 1.5 has been
dropped.
+New tutorials
+~~~~~~~~~~~~~
+
+Additions to the docs include a revamped :doc:`Tutorial 3</intro/tutorial03>`
+and a new :doc:`tutorial on testing</intro/tutorial05>`. A new section,
+"Advanced Tutorials", offers :doc:`How to write reusable apps
+</intro/reusable-apps>` as well as a step-by-step guide for new contributors in
+:doc:`Writing your first patch for Django </intro/contributing>`.
+
Minor features
~~~~~~~~~~~~~~
@@ -296,6 +305,31 @@ Django 1.5 also includes several smaller improvements worth noting:
you to test equality for XML content at a semantic level, without caring for
syntax differences (spaces, attribute order, etc.).
+* RemoteUserMiddleware now forces logout when the REMOTE_USER header
+ disappears during the same browser session.
+
+* The :ref:`cache-based session backend <cached-sessions-backend>` can store
+ session data in a non-default cache.
+
+* Multi-column indexes can now be created on models. Read the
+ :attr:`~django.db.models.Options.index_together` documentation for more
+ information.
+
+* During Django's logging configuration verbose Deprecation warnings are
+ enabled and warnings are captured into the logging system. Logged warnings
+ are routed through the ``console`` logging handler, which by default requires
+ :setting:`DEBUG` to be True for output to be generated. The result is that
+ DeprecationWarnings should be printed to the console in development
+ environments the way they have been in Python versions < 2.7.
+
+* The API for :meth:`django.contrib.admin.ModelAdmin.message_user` method has
+ been modified to accept additional arguments adding capabilities similar to
+ :func:`django.contrib.messages.add_message`. This is useful for generating
+ error messages from admin actions.
+
+* The admin's list filters can now be customized per-request thanks to the new
+ :meth:`django.contrib.admin.ModelAdmin.get_list_filter` method.
+
Backwards incompatible changes in 1.5
=====================================
@@ -307,6 +341,21 @@ Backwards incompatible changes in 1.5
deprecation timeline for a given feature, its removal may appear as a
backwards incompatible change.
+Managers on abstract models
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Abstract models are able to define a custom manager, and that manager
+:ref:`will be inherited by any concrete models extending the abstract model
+<custom-managers-and-inheritance>`. However, if you try to use the abstract
+model to call a method on the manager, an exception will now be raised.
+Previously, the call would have been permitted, but would have failed as soon
+as any database operation was attempted (usually with a "table does not exist"
+error from the database).
+
+If you have functionality on a manager that you have been invoking using
+the abstract class, you should migrate that logic to a Python
+``staticmethod`` or ``classmethod`` on the abstract class.
+
Context in year archive class-based views
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -529,6 +578,20 @@ with the :meth:`~django.forms.Form.is_valid()` method and not with the
presence or absence of the :attr:`~django.forms.Form.cleaned_data` attribute
on the form.
+Behavior of :djadmin:`syncdb` with multiple databases
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+:djadmin:`syncdb` now queries the database routers to determine if content
+types (when :mod:`~django.contrib.contenttypes` is enabled) and permissions
+(when :mod:`~django.contrib.auth` is enabled) should be created in the target
+database. Previously, it created them in the default database, even when
+another database was specified with the :djadminopt:`--database` option.
+
+If you use :djadmin:`syncdb` on multiple databases, you should ensure that
+your routers allow synchronizing content types and permissions to only one of
+them. See the docs on the :ref:`behavior of contrib apps with multiple
+databases <contrib_app_multiple_databases>` for more information.
+
Miscellaneous
~~~~~~~~~~~~~
@@ -553,10 +616,21 @@ Miscellaneous
:ref:`Q() expressions <complex-lookups-with-q>` and ``QuerySet`` combining where
the operators are used as boolean AND and OR operators.
+* In a ``filter()`` call, when :ref:`F() expressions <query-expressions>`
+ contained lookups spanning multi-valued relations, they didn't always reuse
+ the same relations as other lookups along the same chain. This was changed,
+ and now F() expressions will always use the same relations as other lookups
+ within the same ``filter()`` call.
+
* The :ttag:`csrf_token` template tag is no longer enclosed in a div. If you need
HTML validation against pre-HTML5 Strict DTDs, you should add a div around it
in your pages.
+* The template tags library ``adminmedia``, which only contained the
+ deprecated template tag ``{% admin_media_prefix %}``, was removed.
+ Attempting to load it with ``{% load adminmedia %}`` will fail. If your
+ templates still contain that line you must remove it.
+
Features deprecated in 1.5
==========================
@@ -613,7 +687,6 @@ Define a ``__str__`` method and apply the
The :func:`~django.utils.itercompat.product` function has been deprecated. Use
the built-in :func:`itertools.product` instead.
-
``django.utils.markup``
~~~~~~~~~~~~~~~~~~~~~~~
@@ -621,3 +694,22 @@ The markup contrib module has been deprecated and will follow an accelerated
deprecation schedule. Direct use of python markup libraries or 3rd party tag
libraries is preferred to Django maintaining this functionality in the
framework.
+
+``cleanup`` management command
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :djadmin:`cleanup` management command has been deprecated and replaced by
+:djadmin:`clearsessions`.
+
+``daily_cleanup.py`` script
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The undocumented ``daily_cleanup.py`` script has been deprecated. Use the
+:djadmin:`clearsessions` management command instead.
+
+``depth`` keyword argument in ``select_related``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``depth`` keyword argument in
+:meth:`~django.db.models.query.QuerySet.select_related` has been deprecated.
+You should use field names instead.
diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt
new file mode 100644
index 0000000000..1f57913397
--- /dev/null
+++ b/docs/releases/1.6.txt
@@ -0,0 +1,43 @@
+============================================
+Django 1.6 release notes - UNDER DEVELOPMENT
+============================================
+
+Welcome to Django 1.6!
+
+These release notes cover the `new features`_, as well as some `backwards
+incompatible changes`_ you'll want to be aware of when upgrading from Django
+1.5 or older versions. We've also dropped some features, which are detailed in
+:doc:`our deprecation plan </internals/deprecation>`, and we've `begun the
+deprecation process for some features`_.
+
+.. _`new features`: `What's new in Django 1.6`_
+.. _`backwards incompatible changes`: `Backwards incompatible changes in 1.6`_
+.. _`begun the deprecation process for some features`: `Features deprecated in 1.6`_
+
+What's new in Django 1.6
+========================
+
+Minor features
+~~~~~~~~~~~~~~
+
+* Authentication backends can raise ``PermissionDenied`` to immediately fail
+ the authentication chain.
+
+* 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.
+
+Backwards incompatible changes in 1.6
+=====================================
+
+.. warning::
+
+ In addition to the changes outlined in this section, be sure to review the
+ :doc:`deprecation plan </internals/deprecation>` for any features that
+ have been removed. If you haven't updated your code within the
+ deprecation timeline for a given feature, its removal may appear as a
+ backwards incompatible change.
+
+Features deprecated in 1.6
+==========================
diff --git a/docs/releases/index.txt b/docs/releases/index.txt
index 6df9821f56..b0cd87a168 100644
--- a/docs/releases/index.txt
+++ b/docs/releases/index.txt
@@ -16,6 +16,13 @@ Final releases
.. _development_release_notes:
+1.6 release
+-----------
+.. toctree::
+ :maxdepth: 1
+
+ 1.6
+
1.5 release
-----------
.. toctree::
@@ -92,6 +99,7 @@ notes.
.. toctree::
:maxdepth: 1
+ 1.5-beta-1
1.5-alpha-1
1.4-beta-1
1.4-alpha-1
diff --git a/docs/topics/_images/django_unittest_classes_hierarchy.graffle b/docs/topics/_images/django_unittest_classes_hierarchy.graffle
new file mode 100644
index 0000000000..7211c0f3be
--- /dev/null
+++ b/docs/topics/_images/django_unittest_classes_hierarchy.graffle
@@ -0,0 +1,883 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>ActiveLayerIndex</key>
+ <integer>0</integer>
+ <key>ApplicationVersion</key>
+ <array>
+ <string>com.omnigroup.OmniGrafflePro</string>
+ <string>139.16.0.171715</string>
+ </array>
+ <key>AutoAdjust</key>
+ <true/>
+ <key>BackgroundGraphic</key>
+ <dict>
+ <key>Bounds</key>
+ <string>{{0, 0}, {559.28997802734375, 782.8900146484375}}</string>
+ <key>Class</key>
+ <string>SolidGraphic</string>
+ <key>ID</key>
+ <integer>2</integer>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>BaseZoom</key>
+ <integer>0</integer>
+ <key>CanvasOrigin</key>
+ <string>{0, 0}</string>
+ <key>ColumnAlign</key>
+ <integer>1</integer>
+ <key>ColumnSpacing</key>
+ <real>36</real>
+ <key>CreationDate</key>
+ <string>2012-12-16 18:52:14 +0000</string>
+ <key>Creator</key>
+ <string>Aymeric Augustin</string>
+ <key>DisplayScale</key>
+ <string>1.000 cm = 1.000 cm</string>
+ <key>GraphDocumentVersion</key>
+ <integer>8</integer>
+ <key>GraphicsList</key>
+ <array>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>8</integer>
+ </dict>
+ <key>ID</key>
+ <integer>29</integer>
+ <key>OrthogonalBarAutomatic</key>
+ <true/>
+ <key>OrthogonalBarPoint</key>
+ <string>{0, 0}</string>
+ <key>OrthogonalBarPosition</key>
+ <real>-1</real>
+ <key>Points</key>
+ <array>
+ <string>{369, 459}</string>
+ <string>{216, 400.5}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>UMLInheritance</string>
+ <key>HeadScale</key>
+ <real>0.79999995231628418</real>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>2</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>6</integer>
+ <key>Info</key>
+ <integer>2</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>12</integer>
+ <key>Info</key>
+ <integer>1</integer>
+ </dict>
+ <key>ID</key>
+ <integer>27</integer>
+ <key>OrthogonalBarAutomatic</key>
+ <true/>
+ <key>OrthogonalBarPoint</key>
+ <string>{0, 0}</string>
+ <key>OrthogonalBarPosition</key>
+ <real>-1</real>
+ <key>Points</key>
+ <array>
+ <string>{135, 270}</string>
+ <string>{369, 225}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>UMLInheritance</string>
+ <key>HeadScale</key>
+ <real>0.79999995231628418</real>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>2</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>26</integer>
+ <key>Position</key>
+ <real>0.5</real>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>10</integer>
+ </dict>
+ <key>ID</key>
+ <integer>26</integer>
+ <key>OrthogonalBarAutomatic</key>
+ <true/>
+ <key>OrthogonalBarPoint</key>
+ <string>{0, 0}</string>
+ <key>OrthogonalBarPosition</key>
+ <real>-1</real>
+ <key>Points</key>
+ <array>
+ <string>{135, 315}</string>
+ <string>{135, 225}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>UMLInheritance</string>
+ <key>HeadScale</key>
+ <real>0.79999995231628418</real>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>2</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>9</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>9</integer>
+ </dict>
+ <key>ID</key>
+ <integer>25</integer>
+ <key>OrthogonalBarAutomatic</key>
+ <true/>
+ <key>OrthogonalBarPoint</key>
+ <string>{0, 0}</string>
+ <key>OrthogonalBarPosition</key>
+ <real>-1</real>
+ <key>Points</key>
+ <array>
+ <string>{135, 387}</string>
+ <string>{135, 342}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>UMLInheritance</string>
+ <key>HeadScale</key>
+ <real>0.79999995231628418</real>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>2</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>8</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Class</key>
+ <string>LineGraphic</string>
+ <key>Head</key>
+ <dict>
+ <key>ID</key>
+ <integer>8</integer>
+ </dict>
+ <key>ID</key>
+ <integer>23</integer>
+ <key>OrthogonalBarAutomatic</key>
+ <true/>
+ <key>OrthogonalBarPoint</key>
+ <string>{0, 0}</string>
+ <key>OrthogonalBarPosition</key>
+ <real>-1</real>
+ <key>Points</key>
+ <array>
+ <string>{135, 459}</string>
+ <string>{135, 414}</string>
+ </array>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>HeadArrow</key>
+ <string>UMLInheritance</string>
+ <key>HeadScale</key>
+ <real>0.79999995231628418</real>
+ <key>Legacy</key>
+ <true/>
+ <key>LineType</key>
+ <integer>2</integer>
+ <key>TailArrow</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ <key>Tail</key>
+ <dict>
+ <key>ID</key>
+ <integer>7</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{378, 252}, {81, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>22</integer>
+ <key>Shape</key>
+ <string>NoteShape</string>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0.501961</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;\red0\green128\blue0;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\i\fs24 \cf2 Python &lt; 2.7}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ <key>TextRelativeArea</key>
+ <string>{{0, 0}, {1, 1}}</string>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{45, 252}, {81, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>FontInfo</key>
+ <dict>
+ <key>Font</key>
+ <string>Helvetica</string>
+ <key>Size</key>
+ <real>12</real>
+ </dict>
+ <key>ID</key>
+ <integer>20</integer>
+ <key>Shape</key>
+ <string>NoteShape</string>
+ <key>Style</key>
+ <dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0.501961</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;\red0\green128\blue0;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\i\fs24 \cf2 Python \uc0\u8805 2.7}</string>
+ <key>VerticalPad</key>
+ <integer>0</integer>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{288, 198}, {162, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>12</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 TestCase}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{54, 198}, {162, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>10</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 TestCase}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{54, 315}, {162, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>9</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 SimpleTestCase}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{54, 387}, {162, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>8</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 TransactionTestCase}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{54, 459}, {162, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>7</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 TestCase}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{288, 459}, {162, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>6</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 LiveServerTestCase}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{18, 297}, {468, 207}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>13</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict/>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>2</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr
+
+\f0\fs24 \cf0 django.test}</string>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{18, 153}, {225, 90}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>18</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict/>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>2</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr
+
+\f0\fs24 \cf0 django.utils.unittest\
+= unittest (standard library)}</string>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{261, 153}, {225, 90}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>19</integer>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict/>
+ <key>Text</key>
+ <dict>
+ <key>Align</key>
+ <integer>2</integer>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr
+
+\f0\fs24 \cf0 django.utils.unittest\
+= unittest2 (bundled copy)}</string>
+ </dict>
+ <key>TextPlacement</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GridInfo</key>
+ <dict>
+ <key>ShowsGrid</key>
+ <string>YES</string>
+ <key>SnapsToGrid</key>
+ <string>YES</string>
+ </dict>
+ <key>GuidesLocked</key>
+ <string>NO</string>
+ <key>GuidesVisible</key>
+ <string>YES</string>
+ <key>HPages</key>
+ <integer>1</integer>
+ <key>ImageCounter</key>
+ <integer>1</integer>
+ <key>KeepToScale</key>
+ <false/>
+ <key>Layers</key>
+ <array>
+ <dict>
+ <key>Lock</key>
+ <string>NO</string>
+ <key>Name</key>
+ <string>Calque 1</string>
+ <key>Print</key>
+ <string>YES</string>
+ <key>View</key>
+ <string>YES</string>
+ </dict>
+ </array>
+ <key>LayoutInfo</key>
+ <dict>
+ <key>Animate</key>
+ <string>NO</string>
+ <key>circoMinDist</key>
+ <real>18</real>
+ <key>circoSeparation</key>
+ <real>0.0</real>
+ <key>layoutEngine</key>
+ <string>dot</string>
+ <key>neatoSeparation</key>
+ <real>0.0</real>
+ <key>twopiSeparation</key>
+ <real>0.0</real>
+ </dict>
+ <key>LinksVisible</key>
+ <string>NO</string>
+ <key>MagnetsVisible</key>
+ <string>NO</string>
+ <key>MasterSheets</key>
+ <array/>
+ <key>ModificationDate</key>
+ <string>2012-12-16 19:08:28 +0000</string>
+ <key>Modifier</key>
+ <string>Aymeric Augustin</string>
+ <key>NotesVisible</key>
+ <string>NO</string>
+ <key>Orientation</key>
+ <integer>2</integer>
+ <key>OriginVisible</key>
+ <string>NO</string>
+ <key>PageBreaks</key>
+ <string>YES</string>
+ <key>PrintInfo</key>
+ <dict>
+ <key>NSBottomMargin</key>
+ <array>
+ <string>float</string>
+ <string>41</string>
+ </array>
+ <key>NSHorizonalPagination</key>
+ <array>
+ <string>coded</string>
+ <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string>
+ </array>
+ <key>NSLeftMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSPaperSize</key>
+ <array>
+ <string>size</string>
+ <string>{595.28997802734375, 841.8900146484375}</string>
+ </array>
+ <key>NSPrintReverseOrientation</key>
+ <array>
+ <string>int</string>
+ <string>0</string>
+ </array>
+ <key>NSRightMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSTopMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ </dict>
+ <key>PrintOnePage</key>
+ <false/>
+ <key>ReadOnly</key>
+ <string>NO</string>
+ <key>RowAlign</key>
+ <integer>1</integer>
+ <key>RowSpacing</key>
+ <real>36</real>
+ <key>SheetTitle</key>
+ <string>Canevas 1</string>
+ <key>SmartAlignmentGuidesActive</key>
+ <string>YES</string>
+ <key>SmartDistanceGuidesActive</key>
+ <string>YES</string>
+ <key>UniqueID</key>
+ <integer>1</integer>
+ <key>UseEntirePage</key>
+ <false/>
+ <key>VPages</key>
+ <integer>1</integer>
+ <key>WindowInfo</key>
+ <dict>
+ <key>CurrentSheet</key>
+ <integer>0</integer>
+ <key>ExpandedCanvases</key>
+ <array/>
+ <key>Frame</key>
+ <string>{{9, 4}, {694, 874}}</string>
+ <key>ListView</key>
+ <true/>
+ <key>OutlineWidth</key>
+ <integer>142</integer>
+ <key>RightSidebar</key>
+ <false/>
+ <key>ShowRuler</key>
+ <true/>
+ <key>Sidebar</key>
+ <true/>
+ <key>SidebarWidth</key>
+ <integer>120</integer>
+ <key>VisibleRegion</key>
+ <string>{{0, 0}, {559, 735}}</string>
+ <key>Zoom</key>
+ <real>1</real>
+ <key>ZoomValues</key>
+ <array>
+ <array>
+ <string>Canevas 1</string>
+ <real>1</real>
+ <real>1</real>
+ </array>
+ </array>
+ </dict>
+</dict>
+</plist>
diff --git a/docs/topics/_images/django_unittest_classes_hierarchy.pdf b/docs/topics/_images/django_unittest_classes_hierarchy.pdf
new file mode 100644
index 0000000000..cedaba22ac
--- /dev/null
+++ b/docs/topics/_images/django_unittest_classes_hierarchy.pdf
Binary files differ
diff --git a/docs/topics/_images/django_unittest_classes_hierarchy.png b/docs/topics/_images/django_unittest_classes_hierarchy.png
deleted file mode 100644
index 5f28b56ed6..0000000000
--- a/docs/topics/_images/django_unittest_classes_hierarchy.png
+++ /dev/null
Binary files differ
diff --git a/docs/topics/_images/django_unittest_classes_hierarchy.svg b/docs/topics/_images/django_unittest_classes_hierarchy.svg
new file mode 100644
index 0000000000..0482f044dd
--- /dev/null
+++ b/docs/topics/_images/django_unittest_classes_hierarchy.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-2 137 508 391" width="508pt" height="391pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-12-16 19:08Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.2 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Courier" font-size="12" units-per-em="1000" underline-position="-178.22266" underline-thickness="57.617188" slope="0" x-height="462.40234" cap-height="594.72656" ascent="753.90625" descent="-246.09375" font-weight="500"><font-face-src><font-face-name name="Courier"/></font-face-src></font-face><linearGradient x1="0" x2="1" id="Gradient" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="white"/><stop offset="1" stop-color="#aaa"/></linearGradient><linearGradient id="Obj_Gradient" xl:href="#Gradient" gradientTransform="translate(369 459) rotate(90) scale(27)"/><linearGradient id="Obj_Gradient_2" xl:href="#Gradient" gradientTransform="translate(135 459) rotate(90) scale(27)"/><linearGradient id="Obj_Gradient_3" xl:href="#Gradient" gradientTransform="translate(135 387) rotate(90) scale(27)"/><linearGradient id="Obj_Gradient_4" xl:href="#Gradient" gradientTransform="translate(135 315) rotate(90) scale(27)"/><linearGradient id="Obj_Gradient_5" xl:href="#Gradient" gradientTransform="translate(135 198) rotate(90) scale(27)"/><linearGradient id="Obj_Gradient_6" xl:href="#Gradient" gradientTransform="translate(369 198) rotate(90) scale(27)"/><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="-1e3" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-style="italic" font-weight="500"><font-face-src><font-face-name name="Helvetica-Oblique"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="UMLInheritance_Marker" viewBox="-1 -7 12 14" markerWidth="12" markerHeight="14" color="black"><g><path d="M 9.5999994 0 L 0 -5.5999997 L 0 5.5999997 Z" fill="none" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canevas 1</title><rect fill="white" width="559.28998" height="782.89"/><g><title>Calque 1</title><g><use xl:href="#id19_Graphic" filter="url(#Shadow)"/><use xl:href="#id18_Graphic" filter="url(#Shadow)"/><use xl:href="#id13_Graphic" filter="url(#Shadow)"/><use xl:href="#id6_Graphic" filter="url(#Shadow)"/><use xl:href="#id7_Graphic" filter="url(#Shadow)"/><use xl:href="#id8_Graphic" filter="url(#Shadow)"/><use xl:href="#id9_Graphic" filter="url(#Shadow)"/><use xl:href="#id10_Graphic" filter="url(#Shadow)"/><use xl:href="#id12_Graphic" filter="url(#Shadow)"/><use xl:href="#id20_Graphic" filter="url(#Shadow)"/><use xl:href="#id22_Graphic" filter="url(#Shadow)"/></g><g id="id19_Graphic"><rect x="261" y="153" width="225" height="90" fill="white"/><rect x="261" y="153" width="225" height="90" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(266 158)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="63.77539" y="11" textLength="151.22461">django.utils.unittest</tspan><tspan font-family="Courier" font-size="12" font-weight="500" x="27.769531" y="25" textLength="187.23047">= unittest2 (bundled copy)</tspan></text></g><g id="id18_Graphic"><rect x="18" y="153" width="225" height="90" fill="white"/><rect x="18" y="153" width="225" height="90" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(23 158)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="63.77539" y="11" textLength="151.22461">django.utils.unittest</tspan><tspan font-family="Courier" font-size="12" font-weight="500" x="6.1660156" y="25" textLength="208.83398">= unittest (standard library)</tspan></text></g><g id="id13_Graphic"><rect x="18" y="297" width="468" height="207" fill="white"/><rect x="18" y="297" width="468" height="207" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(23 302)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="378.7871" y="11" textLength="79.21289">django.test</tspan></text></g><g id="id6_Graphic"><path d="M 293 459 L 445 459 C 447.76142 459 450 461.23858 450 464 L 450 481 C 450 483.76142 447.76142 486 445 486 L 293 486 C 290.23858 486 288 483.76142 288 481 C 288 481 288 481 288 481 L 288 464 C 288 461.23858 290.23858 459 293 459 C 293 459 293 459 293 459 Z" fill="url(#Obj_Gradient)"/><path d="M 293 459 L 445 459 C 447.76142 459 450 461.23858 450 464 L 450 481 C 450 483.76142 447.76142 486 445 486 L 293 486 C 290.23858 486 288 483.76142 288 481 C 288 481 288 481 288 481 L 288 464 C 288 461.23858 290.23858 459 293 459 C 293 459 293 459 293 459 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(293 465.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="11.189453" y="11" textLength="129.62109">LiveServerTestCase</tspan></text></g><g id="id7_Graphic"><path d="M 59 459 L 211 459 C 213.76142 459 216 461.23858 216 464 L 216 481 C 216 483.76142 213.76142 486 211 486 L 59 486 C 56.238576 486 54 483.76142 54 481 C 54 481 54 481 54 481 L 54 464 C 54 461.23858 56.238576 459 59 459 C 59 459 59 459 59 459 Z" fill="url(#Obj_Gradient_2)"/><path d="M 59 459 L 211 459 C 213.76142 459 216 461.23858 216 464 L 216 481 C 216 483.76142 213.76142 486 211 486 L 59 486 C 56.238576 486 54 483.76142 54 481 C 54 481 54 481 54 481 L 54 464 C 54 461.23858 56.238576 459 59 459 C 59 459 59 459 59 459 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(59 465.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="47.195312" y="11" textLength="57.609375">TestCase</tspan></text></g><g id="id8_Graphic"><path d="M 59 387 L 211 387 C 213.76142 387 216 389.23858 216 392 L 216 409 C 216 411.76142 213.76142 414 211 414 L 59 414 C 56.238576 414 54 411.76142 54 409 C 54 409 54 409 54 409 L 54 392 C 54 389.23858 56.238576 387 59 387 C 59 387 59 387 59 387 Z" fill="url(#Obj_Gradient_3)"/><path d="M 59 387 L 211 387 C 213.76142 387 216 389.23858 216 392 L 216 409 C 216 411.76142 213.76142 414 211 414 L 59 414 C 56.238576 414 54 411.76142 54 409 C 54 409 54 409 54 409 L 54 392 C 54 389.23858 56.238576 387 59 387 C 59 387 59 387 59 387 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(59 393.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="7.588867" y="11" textLength="136.822266">TransactionTestCase</tspan></text></g><g id="id9_Graphic"><path d="M 59 315 L 211 315 C 213.76142 315 216 317.23858 216 320 L 216 337 C 216 339.76142 213.76142 342 211 342 L 59 342 C 56.238576 342 54 339.76142 54 337 C 54 337 54 337 54 337 L 54 320 C 54 317.23858 56.238576 315 59 315 C 59 315 59 315 59 315 Z" fill="url(#Obj_Gradient_4)"/><path d="M 59 315 L 211 315 C 213.76142 315 216 317.23858 216 320 L 216 337 C 216 339.76142 213.76142 342 211 342 L 59 342 C 56.238576 342 54 339.76142 54 337 C 54 337 54 337 54 337 L 54 320 C 54 317.23858 56.238576 315 59 315 C 59 315 59 315 59 315 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(59 321.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="25.591797" y="11" textLength="100.816406">SimpleTestCase</tspan></text></g><g id="id10_Graphic"><path d="M 59 198 L 211 198 C 213.76142 198 216 200.23858 216 203 L 216 220 C 216 222.76142 213.76142 225 211 225 L 59 225 C 56.238576 225 54 222.76142 54 220 C 54 220 54 220 54 220 L 54 203 C 54 200.23858 56.238576 198 59 198 C 59 198 59 198 59 198 Z" fill="url(#Obj_Gradient_5)"/><path d="M 59 198 L 211 198 C 213.76142 198 216 200.23858 216 203 L 216 220 C 216 222.76142 213.76142 225 211 225 L 59 225 C 56.238576 225 54 222.76142 54 220 C 54 220 54 220 54 220 L 54 203 C 54 200.23858 56.238576 198 59 198 C 59 198 59 198 59 198 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(59 204.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="47.195312" y="11" textLength="57.609375">TestCase</tspan></text></g><g id="id12_Graphic"><path d="M 293 198 L 445 198 C 447.76142 198 450 200.23858 450 203 L 450 220 C 450 222.76142 447.76142 225 445 225 L 293 225 C 290.23858 225 288 222.76142 288 220 C 288 220 288 220 288 220 L 288 203 C 288 200.23858 290.23858 198 293 198 C 293 198 293 198 293 198 Z" fill="url(#Obj_Gradient_6)"/><path d="M 293 198 L 445 198 C 447.76142 198 450 200.23858 450 203 L 450 220 C 450 222.76142 447.76142 225 445 225 L 293 225 C 290.23858 225 288 222.76142 288 220 C 288 220 288 220 288 220 L 288 203 C 288 200.23858 290.23858 198 293 198 C 293 198 293 198 293 198 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(293 204.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="47.195312" y="11" textLength="57.609375">TestCase</tspan></text></g><g id="id20_Graphic"><path d="M 126 259.9893 C 126 257.43159 124.9713 257.11515 117.45693 254.5839 L 117.37755 254.55771 C 109.82349 252 109.74492 252 101.99241 252 C 91.66977 252 45 252 45 252 L 45 279 L 126 279 L 126 259.9893 Z" fill="white"/><path d="M 126 259.9893 C 126 257.43159 124.9713 257.11515 117.45693 254.5839 L 117.37755 254.55771 C 109.82349 252 109.74492 252 101.99241 252 C 91.66977 252 45 252 45 252 L 45 279 L 126 279 L 126 259.9893 Z M 126 259.85754 C 126 257.43159 125.92062 257.43159 109.74492 257.43159 L 109.74492 257.43159 C 109.74492 252.02646 109.74492 252 102.46707 252" stroke="green" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(50 258.5)" fill="green"><tspan font-family="Helvetica" font-size="12" font-style="italic" font-weight="500" fill="green" x="1.8525391" y="11" textLength="67.29492">Python ≥ 2.7</tspan></text></g><g id="id22_Graphic"><path d="M 459 259.9893 C 459 257.43159 457.9713 257.11515 450.45693 254.5839 L 450.37755 254.55771 C 442.8235 252 442.74492 252 434.9924 252 C 424.66977 252 378 252 378 252 L 378 279 L 459 279 L 459 259.9893 Z" fill="white"/><path d="M 459 259.9893 C 459 257.43159 457.9713 257.11515 450.45693 254.5839 L 450.37755 254.55771 C 442.8235 252 442.74492 252 434.9924 252 C 424.66977 252 378 252 378 252 L 378 279 L 459 279 L 459 259.9893 Z M 459 259.85754 C 459 257.43159 458.92062 257.43159 442.74492 257.43159 L 442.74492 257.43159 C 442.74492 252.02646 442.74492 252 435.46707 252" stroke="green" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(383 258.5)" fill="green"><tspan font-family="Helvetica" font-size="12" font-style="italic" font-weight="500" fill="green" x="1.6416016" y="11" textLength="67.716797">Python &lt; 2.7</tspan></text></g><path d="M 135 459 L 135 445.9 L 135 427.1 L 135 425.1" marker-end="url(#UMLInheritance_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 135 387 L 135 373.9 L 135 355.1 L 135 353.1" marker-end="url(#UMLInheritance_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 135 315 L 135 301.9 L 135 238.1 L 135 236.1" marker-end="url(#UMLInheritance_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 135 270 L 148.1 270 L 369 270 L 369 238.1 L 369 236.1" marker-end="url(#UMLInheritance_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 369 459 L 369 445.9 L 369 400.5 L 229.1 400.5 L 227.1 400.5" marker-end="url(#UMLInheritance_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g></g></svg>
diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt
index 41159984f6..3e2b6bbdbf 100644
--- a/docs/topics/auth.txt
+++ b/docs/topics/auth.txt
@@ -408,7 +408,7 @@ installation supports. The first entry in this list (that is,
``settings.PASSWORD_HASHERS[0]``) will be used to store passwords, and all the
other entries are valid hashers that can be used to check existing passwords.
This means that if you want to use a different algorithm, you'll need to modify
-:setting:`PASSWORD_HASHERS` to list your prefered algorithm first in the list.
+:setting:`PASSWORD_HASHERS` to list your preferred algorithm first in the list.
The default for :setting:`PASSWORD_HASHERS` is::
@@ -1889,7 +1889,7 @@ password resets. You must then provide some key implementation details:
as the identifying field::
class MyUser(AbstractBaseUser):
- identfier = models.CharField(max_length=40, unique=True, db_index=True)
+ identifier = models.CharField(max_length=40, unique=True, db_index=True)
...
USERNAME_FIELD = 'identifier'
@@ -1911,6 +1911,15 @@ password resets. You must then provide some key implementation details:
``REQUIRED_FIELDS`` must contain all required fields on your User
model, but should *not* contain the ``USERNAME_FIELD``.
+ .. attribute:: User.is_active
+
+ A boolean attribute that indicates whether the user is considered
+ "active". This attribute is provided as an attribute on
+ ``AbstractBaseUser`` defaulting to ``True``. How you choose to
+ implement it will depend on the details of your chosen auth backends.
+ See the documentation of the :attr:`attribute on the builtin user model
+ <django.contrib.auth.models.User.is_active>` for details.
+
.. method:: User.get_full_name():
A longer formal identifier for the user. A common interpretation
@@ -2000,7 +2009,7 @@ additional methods:
.. method:: models.CustomUserManager.create_superuser(*username_field*, password, **other_fields)
- The prototype of `create_user()` should accept the username field,
+ The prototype of `create_superuser()` should accept the username field,
plus all required fields as arguments. For example, if your user model
uses `email` as the username field, and has `date_of_birth` as a required
fields, then create_superuser should be defined as::
@@ -2127,6 +2136,76 @@ override any of the definitions that refer to fields on
:class:`~django.contrib.auth.models.AbstractUser` that aren't on your
custom User class.
+Custom users and permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To make it easy to include Django's permission framework into your own User
+class, Django provides :class:`~django.contrib.auth.model.PermissionsMixin`.
+This is an abstract model you can include in the class heirarchy for your User
+model, giving you all the methods and database fields necessary to support
+Django's permission model.
+
+:class:`~django.contrib.auth.model.PermissionsMixin` provides the following
+methods and attributes:
+
+.. class:: models.PermissionsMixin
+
+ .. attribute:: models.PermissionsMixin.is_superuser
+
+ Boolean. Designates that this user has all permissions without
+ explicitly assigning them.
+
+ .. method:: models.PermissionsMixin.get_group_permissions(obj=None)
+
+ Returns a set of permission strings that the user has, through his/her
+ groups.
+
+ If ``obj`` is passed in, only returns the group permissions for
+ this specific object.
+
+ .. method:: models.PermissionsMixin.get_all_permissions(obj=None)
+
+ Returns a set of permission strings that the user has, both through
+ group and user permissions.
+
+ If ``obj`` is passed in, only returns the permissions for this
+ specific object.
+
+ .. method:: models.PermissionsMixin.has_perm(perm, obj=None)
+
+ Returns ``True`` if the user has the specified permission, where perm is
+ in the format ``"<app label>.<permission codename>"`` (see
+ `permissions`_). If the user is inactive, this method will
+ always return ``False``.
+
+ If ``obj`` is passed in, this method won't check for a permission for
+ the model, but for this specific object.
+
+ .. method:: models.PermissionsMixin.has_perms(perm_list, obj=None)
+
+ Returns ``True`` if the user has each of the specified permissions,
+ where each perm is in the format
+ ``"<app label>.<permission codename>"``. If the user is inactive,
+ this method will always return ``False``.
+
+ If ``obj`` is passed in, this method won't check for permissions for
+ the model, but for the specific object.
+
+ .. method:: models.PermissionsMixin.has_module_perms(package_name)
+
+ Returns ``True`` if the user has any permissions in the given package
+ (the Django app label). If the user is inactive, this method will
+ always return ``False``.
+
+.. admonition:: ModelBackend
+
+ If you don't include the
+ :class:`~django.contrib.auth.model.PermissionsMixin`, you must ensure you
+ don't invoke the permissions methods on ``ModelBackend``. ``ModelBackend``
+ assumes that certain fields are available on your user model. If your User
+ model doesn't provide those fields, you will receive database errors when
+ you check permissions.
+
Custom users and Proxy models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2139,6 +2218,68 @@ If your project uses proxy models, you must either modify the proxy to extend
the User model that is currently in use in your project, or merge your proxy's
behavior into your User subclass.
+Custom users and signals
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Another limitation of custom User models is that you can't use
+:func:`django.contrib.auth.get_user_model()` as the sender or target of a signal
+handler. Instead, you must register the handler with the actual User model.
+
+Custom users and testing/fixtures
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you are writing an application that interacts with the User model, you must
+take some precautions to ensure that your test suite will run regardless of
+the User model that is being used by a project. Any test that instantiates an
+instance of User will fail if the User model has been swapped out. This
+includes any attempt to create an instance of User with a fixture.
+
+To ensure that your test suite will pass in any project configuration,
+``django.contrib.auth.tests.utils`` defines a ``@skipIfCustomUser`` decorator.
+This decorator will cause a test case to be skipped if any User model other
+than the default Django user is in use. This decorator can be applied to a
+single test, or to an entire test class.
+
+Depending on your application, tests may also be needed to be added to ensure
+that the application works with *any* user model, not just the default User
+model. To assist with this, Django provides two substitute user models that
+can be used in test suites:
+
+* :class:`django.contrib.auth.tests.custom_user.CustomUser`, a custom user
+ model that uses an ``email`` field as the username, and has a basic
+ admin-compliant permissions setup
+
+* :class:`django.contrib.auth.tests.custom_user.ExtensionUser`, a custom
+ user model that extends :class:`~django.contrib.auth.models.AbstractUser`,
+ adding a ``date_of_birth`` field.
+
+You can then use the ``@override_settings`` decorator to make that test run
+with the custom User model. For example, here is a skeleton for a test that
+would test three possible User models -- the default, plus the two User
+models provided by ``auth`` app::
+
+ from django.contrib.auth.tests.utils import skipIfCustomUser
+ from django.test import TestCase
+ from django.test.utils import override_settings
+
+
+ class ApplicationTestCase(TestCase):
+ @skipIfCustomUser
+ def test_normal_user(self):
+ "Run tests for the normal user model"
+ self.assertSomething()
+
+ @override_settings(AUTH_USER_MODEL='auth.CustomUser')
+ def test_custom_user(self):
+ "Run tests for a custom user model with email-based authentication"
+ self.assertSomething()
+
+ @override_settings(AUTH_USER_MODEL='auth.ExtensionUser')
+ def test_extension_user(self):
+ "Run tests for a simple extension of the built-in User."
+ self.assertSomething()
+
+
A full example
--------------
@@ -2272,15 +2413,21 @@ code would be required in the app's ``admin.py`` file::
class UserChangeForm(forms.ModelForm):
- """A form for updateing users. Includes all the fields on
+ """A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
- pasword hash display field.
+ password hash display field.
"""
password = ReadOnlyPasswordHashField()
class Meta:
model = MyUser
+ def clean_password(self):
+ # Regardless of what the user provides, return the initial value.
+ # This is done here, rather than on the field, because the
+ # field does not have access to the initial value
+ return self.initial["password"]
+
class MyUserAdmin(UserAdmin):
# The forms to add and change user instances
@@ -2376,6 +2523,12 @@ processing at the first positive match.
you need to force users to re-authenticate using different methods. A simple
way to do that is simply to execute ``Session.objects.all().delete()``.
+.. versionadded:: 1.6
+
+If a backend raises a :class:`~django.core.exceptions.PermissionDenied`
+exception, authentication will immediately fail. Django won't check the
+backends that follow.
+
Writing an authentication backend
---------------------------------
diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt
index 2f95c33dd5..a15cf58370 100644
--- a/docs/topics/cache.txt
+++ b/docs/topics/cache.txt
@@ -785,6 +785,16 @@ nonexistent cache key.::
However, if the backend doesn't natively provide an increment/decrement
operation, it will be implemented using a two-step retrieve/update.
+
+You can close the connection to your cache with ``close()`` if implemented by
+the cache backend.
+
+ >>> cache.close()
+
+.. note::
+
+ For caches that don't implement ``close`` methods it is a no-op.
+
.. _cache_key_prefixing:
Cache key prefixing
diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt
index 7bae3c692d..7d12184705 100644
--- a/docs/topics/class-based-views/generic-editing.txt
+++ b/docs/topics/class-based-views/generic-editing.txt
@@ -90,8 +90,8 @@ class:
.. code-block:: python
# models.py
- from django import models
from django.core.urlresolvers import reverse
+ from django.db import models
class Author(models.Model):
name = models.CharField(max_length=200)
@@ -102,7 +102,7 @@ class:
Then we can use :class:`CreateView` and friends to do the actual
work. Notice how we're just configuring the generic class-based views
here; we don't have to write any logic ourselves::
-
+
# views.py
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.core.urlresolvers import reverse_lazy
@@ -134,7 +134,7 @@ Finally, we hook these new views into the URLconf::
url(r'author/(?P<pk>\d+)/$', AuthorUpdate.as_view(), name='author_update'),
url(r'author/(?P<pk>\d+)/delete/$', AuthorDelete.as_view(), name='author_delete'),
)
-
+
.. note::
These views inherit :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`
@@ -160,8 +160,8 @@ you can use a custom :class:`ModelForm` to do this. First, add the
foreign key relation to the model::
# models.py
- from django import models
from django.contrib.auth import User
+ from django.db import models
class Author(models.Model):
name = models.CharField(max_length=200)
@@ -177,7 +177,7 @@ Create a custom :class:`ModelForm` in order to exclude the
# forms.py
from django import forms
from myapp.models import Author
-
+
class AuthorForm(forms.ModelForm):
class Meta:
model = Author
@@ -190,7 +190,7 @@ In the view, use the custom :attr:`form_class` and override
from django.views.generic.edit import CreateView
from myapp.models import Author
from myapp.forms import AuthorForm
-
+
class AuthorCreate(CreateView):
form_class = AuthorForm
model = Author
diff --git a/docs/topics/class-based-views/index.txt b/docs/topics/class-based-views/index.txt
index a738221892..54d4b0f252 100644
--- a/docs/topics/class-based-views/index.txt
+++ b/docs/topics/class-based-views/index.txt
@@ -24,9 +24,9 @@ Basic examples
Django provides base view classes which will suit a wide range of applications.
All views inherit from the :class:`~django.views.generic.base.View` class, which
handles linking the view in to the URLs, HTTP method dispatching and other
-simple features. :class:`~django.views.generic.base.RedirectView` is for a simple HTTP
-redirect, and :class:`~django.views.generic.base.TemplateView` extends the base class
-to make it also render a template.
+simple features. :class:`~django.views.generic.base.RedirectView` is for a
+simple HTTP redirect, and :class:`~django.views.generic.base.TemplateView`
+extends the base class to make it also render a template.
Simple usage in your URLconf
@@ -34,7 +34,8 @@ Simple usage in your URLconf
The simplest way to use generic views is to create them directly in your
URLconf. If you're only changing a few simple attributes on a class-based view,
-you can simply pass them into the ``as_view`` method call itself::
+you can simply pass them into the
+:meth:`~django.views.generic.base.View.as_view` method call itself::
from django.conf.urls import patterns, url, include
from django.views.generic import TemplateView
@@ -43,9 +44,10 @@ you can simply pass them into the ``as_view`` method call itself::
(r'^about/', TemplateView.as_view(template_name="about.html")),
)
-Any arguments given will override the ``template_name`` on the
-A similar overriding pattern can be used for the ``url`` attribute on
-:class:`~django.views.generic.base.RedirectView`.
+Any arguments passed to :meth:`~django.views.generic.base.View.as_view` will
+override attributes set on the class. In this example, we set ``template_name``
+on the ``TemplateView``. A similar overriding pattern can be used for the
+``url`` attribute on :class:`~django.views.generic.base.RedirectView`.
Subclassing generic views
@@ -67,8 +69,8 @@ and override the template name::
Then we just need to add this new view into our URLconf.
`~django.views.generic.base.TemplateView` is a class, not a function, so we
-point the URL to the ``as_view`` class method instead, which provides a
-function-like entry to class-based views::
+point the URL to the :meth:`~django.views.generic.base.View.as_view` class
+method instead, which provides a function-like entry to class-based views::
# urls.py
from django.conf.urls import patterns, url, include
diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt
index eda6f9dac0..a14616a17c 100644
--- a/docs/topics/db/managers.txt
+++ b/docs/topics/db/managers.txt
@@ -274,6 +274,21 @@ it into the inheritance hierarchy *after* the defaults::
# Default manager is CustomManager, but OtherManager is
# also available via the "extra_manager" attribute.
+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::
+
+ ClassA.objects.do_something()
+
+is legal, but::
+
+ AbstractBase.objects.do_something()
+
+will raise an exception. This is because managers are intended to encapsulate
+logic for managing collections of objects. Since you can't have a collection of
+abstract objects, it doesn't make sense to be managing them. If you have
+functionality that applies to the abstract model, you should put that functionality
+in a ``staticmethod`` or ``classmethod`` on the abstract model.
+
Implementation concerns
-----------------------
diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt
index 84b4c84061..2f1676ac1a 100644
--- a/docs/topics/db/models.txt
+++ b/docs/topics/db/models.txt
@@ -673,11 +673,12 @@ For example, this model has a few custom methods::
def baby_boomer_status(self):
"Returns the person's baby-boomer status."
import datetime
- if datetime.date(1945, 8, 1) <= self.birth_date <= datetime.date(1964, 12, 31):
- return "Baby boomer"
if self.birth_date < datetime.date(1945, 8, 1):
return "Pre-boomer"
- return "Post-boomer"
+ elif self.birth_date < datetime.date(1965, 1, 1):
+ return "Baby boomer"
+ else:
+ return "Post-boomer"
def is_midwestern(self):
"Returns True if this person is from the Midwest."
diff --git a/docs/topics/db/multi-db.txt b/docs/topics/db/multi-db.txt
index d2ff8645a9..8a02305376 100644
--- a/docs/topics/db/multi-db.txt
+++ b/docs/topics/db/multi-db.txt
@@ -630,3 +630,49 @@ However, if you're using SQLite or MySQL with MyISAM tables, there is
no enforced referential integrity; as a result, you may be able to
'fake' cross database foreign keys. However, this configuration is not
officially supported by Django.
+
+.. _contrib_app_multiple_databases:
+
+Behavior of contrib apps
+------------------------
+
+Several contrib apps include models, and some apps depend on others. Since
+cross-database relationships are impossible, this creates some restrictions on
+how you can split these models across databases:
+
+- each one of ``contenttypes.ContentType``, ``sessions.Session`` and
+ ``sites.Site`` can be stored in any database, given a suitable router.
+- ``auth`` models — ``User``, ``Group`` and ``Permission`` — are linked
+ together and linked to ``ContentType``, so they must be stored in the same
+ database as ``ContentType``.
+- ``admin`` and ``comments`` depend on ``auth``, so their models must be in
+ the same database as ``auth``.
+- ``flatpages`` and ``redirects`` depend on ``sites``, so their models must be
+ in the same database as ``sites``.
+
+In addition, some objects are automatically created just after
+:djadmin:`syncdb` creates a table to hold them in a database:
+
+- a default ``Site``,
+- a ``ContentType`` for each model (including those not stored in that
+ database),
+- three ``Permission`` for each model (including those not stored in that
+ database).
+
+.. versionchanged:: 1.5
+ Previously, ``ContentType`` and ``Permission`` instances were created only
+ in the default database.
+
+For common setups with multiple databases, it isn't useful to have these
+objects in more than one database. Common setups include master / slave and
+connecting to external databases. Therefore, it's recommended:
+
+- either to run :djadmin:`syncdb` only for the default database;
+- or to write :ref:`database router<topics-db-multi-db-routing>` that allows
+ synchronizing these three models only to one database.
+
+.. warning::
+
+ If you're synchronizing content types to more that one database, be aware
+ that their primary keys may not match across databases. This may result in
+ data corruption or data loss.
diff --git a/docs/topics/db/optimization.txt b/docs/topics/db/optimization.txt
index 772792d39d..b5cca52e23 100644
--- a/docs/topics/db/optimization.txt
+++ b/docs/topics/db/optimization.txt
@@ -132,6 +132,41 @@ Write your own :doc:`custom SQL to retrieve data or populate models
</topics/db/sql>`. Use ``django.db.connection.queries`` to find out what Django
is writing for you and start from there.
+Retrieve individual objects using a unique, indexed column
+==========================================================
+
+There are two reasons to use a column with
+:attr:`~django.db.models.Field.unique` or
+:attr:`~django.db.models.Field.db_index` when using
+:meth:`~django.db.models.query.QuerySet.get` to retrieve individual objects.
+First, the query will be quicker because of the underlying database index.
+Also, the query could run much slower if multiple objects match the lookup;
+having a unique constraint on the column guarantees this will never happen.
+
+So using the :ref:`example Weblog models <queryset-model-example>`::
+
+ >>> entry = Entry.objects.get(id=10)
+
+will be quicker than:
+
+ >>> entry = Entry.object.get(headline="News Item Title")
+
+because ``id`` is indexed by the database and is guaranteed to be unique.
+
+Doing the following is potentially quite slow:
+
+ >>> entry = Entry.objects.get(headline__startswith="News")
+
+First of all, `headline` is not indexed, which will make the underlying
+database fetch slower.
+
+Second, the lookup doesn't guarantee that only one object will be returned.
+If the query matches more than one object, it will retrieve and transfer all of
+them from the database. This penalty could be substantial if hundreds or
+thousands of records are returned. The penalty will be compounded if the
+database lives on a separate server, where network overhead and latency also
+play a factor.
+
Retrieve everything at once if you know you will need it
========================================================
diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt
index d826a39562..90c06ac66a 100644
--- a/docs/topics/db/queries.txt
+++ b/docs/topics/db/queries.txt
@@ -417,7 +417,7 @@ translates (roughly) into the following SQL::
There's one exception though, in case of a
:class:`~django.db.models.ForeignKey` you can specify the field
name suffixed with ``_id``. In this case, the value parameter is expected
- to contain the raw value of the foreign model's primary key. For example::
+ to contain the raw value of the foreign model's primary key. For example:
>>> Entry.objects.filter(blog_id__exact=4)
@@ -575,12 +575,17 @@ To select all blogs that contain an entry with *"Lennon"* in the headline
Blog.objects.filter(entry__headline__contains='Lennon').filter(
entry__pub_date__year=2008)
-In this second example, the first filter restricted the queryset to all those
-blogs linked to that particular type of entry. The second filter restricted
-the set of blogs *further* to those that are also linked to the second type of
-entry. The entries select by the second filter may or may not be the same as
-the entries in the first filter. We are filtering the ``Blog`` items with each
-filter statement, not the ``Entry`` items.
+Suppose there is only one blog that had both entries containing *"Lennon"* and
+entries from 2008, but that none of the entries from 2008 contained *"Lennon"*.
+The first query would not return any blogs, but the second query would return
+that one blog.
+
+In the second example, the first filter restricts the queryset to all those
+blogs linked to entries with *"Lennon"* in the headline. The second filter
+restricts the set of blogs *further* to those that are also linked to entries
+that were published in 2008. The entries selected by the second filter may or
+may not be the same as the entries in the first filter. We are filtering the
+``Blog`` items with each filter statement, not the ``Entry`` items.
All of this behavior also applies to
:meth:`~django.db.models.query.QuerySet.exclude`: all the conditions in a
@@ -920,7 +925,7 @@ Things get more complicated if you use inheritance. Consider a subclass of
class ThemeBlog(Blog):
theme = models.CharField(max_length=200)
- django_blog = ThemeBlog(name='Django', tagline='Django is easy', theme = 'python')
+ django_blog = ThemeBlog(name='Django', tagline='Django is easy', theme='python')
django_blog.save() # django_blog.pk == 3
Due to how inheritance works, you have to set both ``pk`` and ``id`` to None::
diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt
index 4a52c5af35..65944abb8b 100644
--- a/docs/topics/db/transactions.txt
+++ b/docs/topics/db/transactions.txt
@@ -161,8 +161,12 @@ managers, too.
transactions. It tells Django you'll be managing the transaction on your
own.
- If your view changes data and doesn't ``commit()`` or ``rollback()``,
- Django will raise a ``TransactionManagementError`` exception.
+ Whether you are writing or simply reading from the database, you must
+ ``commit()`` or ``rollback()`` explicitly or Django will raise a
+ :exc:`TransactionManagementError` exception. This is required when reading
+ from the database because ``SELECT`` statements may call functions which
+ modify tables, and thus it is impossible to know if any data has been
+ modified.
Manual transaction management looks like this::
@@ -204,11 +208,13 @@ This applies to all database operations, not just write operations. Even
if your transaction only reads from the database, the transaction must
be committed or rolled back before you complete a request.
+.. _deactivate-transaction-management:
+
How to globally deactivate transaction management
=================================================
Control freaks can totally disable all transaction management by setting
-``DISABLE_TRANSACTION_MANAGEMENT`` to ``True`` in the Django settings file.
+:setting:`TRANSACTIONS_MANAGED` to ``True`` in the Django settings file.
If you do this, Django won't provide any automatic transaction management
whatsoever. Middleware will no longer implicitly commit transactions, and
diff --git a/docs/topics/files.txt b/docs/topics/files.txt
index c9b4327941..66e104759a 100644
--- a/docs/topics/files.txt
+++ b/docs/topics/files.txt
@@ -139,6 +139,8 @@ useful -- you can use the global default storage system::
See :doc:`/ref/files/storage` for the file storage API.
+.. _builtin-fs-storage:
+
The built-in filesystem storage class
-------------------------------------
diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt
index 692be7cd7c..233346db0d 100644
--- a/docs/topics/forms/modelforms.txt
+++ b/docs/topics/forms/modelforms.txt
@@ -90,9 +90,6 @@ Model field Form field
``NullBooleanField`` ``CharField``
-``PhoneNumberField`` ``USPhoneNumberField``
- (from ``django.contrib.localflavor.us``)
-
``PositiveIntegerField`` ``IntegerField``
``PositiveSmallIntegerField`` ``IntegerField``
@@ -192,6 +189,8 @@ we'll discuss in a moment.)::
name = forms.CharField(max_length=100)
authors = forms.ModelMultipleChoiceField(queryset=Author.objects.all())
+.. _modelform-is-valid-and-errors:
+
The ``is_valid()`` method and ``errors``
----------------------------------------
@@ -213,7 +212,9 @@ method. This method creates and saves a database object from the data
bound to the form. A subclass of ``ModelForm`` can accept an existing
model instance as the keyword argument ``instance``; if this is
supplied, ``save()`` will update that instance. If it's not supplied,
-``save()`` will create a new instance of the specified model::
+``save()`` will create a new instance of the specified model:
+
+.. code-block:: python
# Create a form instance from POST data.
>>> f = ArticleForm(request.POST)
@@ -232,8 +233,10 @@ supplied, ``save()`` will update that instance. If it's not supplied,
>>> f = ArticleForm(request.POST, instance=a)
>>> f.save()
-Note that ``save()`` will raise a ``ValueError`` if the data in the form
-doesn't validate -- i.e., if form.errors evaluates to True.
+Note that if the form :ref:`hasn't been validated
+<modelform-is-valid-and-errors>`, calling ``save()`` will do so by checking
+``form.errors``. A ``ValueError`` will be raised if the data in the form
+doesn't validate -- i.e., if ``form.errors`` evaluates to ``True``.
This ``save()`` method accepts an optional ``commit`` keyword argument, which
accepts either ``True`` or ``False``. If you call ``save()`` with
diff --git a/docs/topics/http/_images/middleware.graffle b/docs/topics/http/_images/middleware.graffle
new file mode 100644
index 0000000000..4aa85f7611
--- /dev/null
+++ b/docs/topics/http/_images/middleware.graffle
@@ -0,0 +1,957 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>ActiveLayerIndex</key>
+ <integer>0</integer>
+ <key>ApplicationVersion</key>
+ <array>
+ <string>com.omnigroup.OmniGrafflePro</string>
+ <string>139.16.0.171715</string>
+ </array>
+ <key>AutoAdjust</key>
+ <true/>
+ <key>BackgroundGraphic</key>
+ <dict>
+ <key>Bounds</key>
+ <string>{{0, 0}, {559.28997802734375, 782.8900146484375}}</string>
+ <key>Class</key>
+ <string>SolidGraphic</string>
+ <key>ID</key>
+ <integer>2</integer>
+ <key>Style</key>
+ <dict>
+ <key>shadow</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Draws</key>
+ <string>NO</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>BaseZoom</key>
+ <integer>0</integer>
+ <key>CanvasOrigin</key>
+ <string>{0, 0}</string>
+ <key>ColumnAlign</key>
+ <integer>1</integer>
+ <key>ColumnSpacing</key>
+ <real>36</real>
+ <key>CreationDate</key>
+ <string>2012-12-09 18:55:12 +0000</string>
+ <key>Creator</key>
+ <string>Aymeric Augustin</string>
+ <key>DisplayScale</key>
+ <string>1.000 cm = 1.000 cm</string>
+ <key>GraphDocumentVersion</key>
+ <integer>8</integer>
+ <key>GraphicsList</key>
+ <array>
+ <dict>
+ <key>Bounds</key>
+ <string>{{144, 405}, {369, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>33</integer>
+ <key>Shape</key>
+ <string>Bezier</string>
+ <key>ShapeData</key>
+ <dict>
+ <key>UnitPoints</key>
+ <array>
+ <string>{-0.5, -0.5}</string>
+ <string>{-0.5, -0.5}</string>
+ <string>{0.47959183673469341, -0.5}</string>
+ <string>{0.47959183673469408, -0.5}</string>
+ <string>{0.47959183673469341, -0.5}</string>
+ <string>{0.5, 0}</string>
+ <string>{0.5, 0}</string>
+ <string>{0.5, 0}</string>
+ <string>{0.47959183673469408, 0.5}</string>
+ <string>{0.47959183673469408, 0.5}</string>
+ <string>{0.47959183673469408, 0.5}</string>
+ <string>{-0.5, 0.5}</string>
+ <string>{-0.5, 0.5}</string>
+ <string>{-0.5, 0.5}</string>
+ <string>{-0.47560975609756084, 0}</string>
+ <string>{-0.47560975609756084, 0}</string>
+ <string>{-0.47560975609756084, 0}</string>
+ <string>{-0.5, -0.5}</string>
+ </array>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 view function}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{229.5, 238.5}, {297, 36}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>31</integer>
+ <key>Rotation</key>
+ <real>270</real>
+ <key>Shape</key>
+ <string>AdjustableArrow</string>
+ <key>ShapeData</key>
+ <dict>
+ <key>width</key>
+ <real>27</real>
+ </dict>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.8</string>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ <key>MiddleFraction</key>
+ <real>0.70634919404983521</real>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.4</string>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>Fuzziness</key>
+ <real>0.0</real>
+ <key>ShadowVector</key>
+ <string>{0, 2}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ <key>Pattern</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;\red255\green0\blue0;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf2 process_exception}</string>
+ </dict>
+ <key>TextRelativeArea</key>
+ <string>{{0.125, 0.25}, {0.75, 0.5}}</string>
+ <key>isConnectedShape</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{328.5, 229.5}, {315, 36}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>30</integer>
+ <key>Rotation</key>
+ <real>270</real>
+ <key>Shape</key>
+ <string>AdjustableArrow</string>
+ <key>ShapeData</key>
+ <dict>
+ <key>width</key>
+ <real>27</real>
+ </dict>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.8</string>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ <key>MiddleFraction</key>
+ <real>0.70634919404983521</real>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.4</string>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>Fuzziness</key>
+ <real>0.0</real>
+ <key>ShadowVector</key>
+ <string>{0, 2}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0.501961</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;\red0\green128\blue0;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf2 process_response}</string>
+ </dict>
+ <key>TextRelativeArea</key>
+ <string>{{0.125, 0.25}, {0.75, 0.5}}</string>
+ <key>isConnectedShape</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{283.5, 238.5}, {297, 36}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>29</integer>
+ <key>Rotation</key>
+ <real>270</real>
+ <key>Shape</key>
+ <string>AdjustableArrow</string>
+ <key>ShapeData</key>
+ <dict>
+ <key>width</key>
+ <real>27</real>
+ </dict>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.8</string>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ <key>MiddleFraction</key>
+ <real>0.70634919404983521</real>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.4</string>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>Fuzziness</key>
+ <real>0.0</real>
+ <key>ShadowVector</key>
+ <string>{0, 2}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0.501961</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ <key>Pattern</key>
+ <integer>1</integer>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;\red0\green128\blue0;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf2 process_template_response}</string>
+ </dict>
+ <key>TextRelativeArea</key>
+ <string>{{0.125, 0.25}, {0.75, 0.5}}</string>
+ <key>isConnectedShape</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{27, 243}, {288, 36}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>28</integer>
+ <key>Rotation</key>
+ <real>90</real>
+ <key>Shape</key>
+ <string>AdjustableArrow</string>
+ <key>ShapeData</key>
+ <dict>
+ <key>width</key>
+ <real>27</real>
+ </dict>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.8</string>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ <key>MiddleFraction</key>
+ <real>0.70634919404983521</real>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.4</string>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>Fuzziness</key>
+ <real>0.0</real>
+ <key>ShadowVector</key>
+ <string>{0, 2}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0.501961</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;\red0\green128\blue0;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf2 process_view}</string>
+ </dict>
+ <key>TextRelativeArea</key>
+ <string>{{0.125, 0.25}, {0.75, 0.5}}</string>
+ <key>isConnectedShape</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{-40.500000000767386, 220.49999999804004}, {297, 36}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>27</integer>
+ <key>Rotation</key>
+ <real>90</real>
+ <key>Shape</key>
+ <string>AdjustableArrow</string>
+ <key>ShapeData</key>
+ <dict>
+ <key>width</key>
+ <real>27</real>
+ </dict>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.8</string>
+ <key>b</key>
+ <string>1</string>
+ <key>g</key>
+ <string>1</string>
+ <key>r</key>
+ <string>1</string>
+ </dict>
+ <key>MiddleFraction</key>
+ <real>0.70634919404983521</real>
+ </dict>
+ <key>shadow</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>a</key>
+ <string>0.4</string>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ <key>Draws</key>
+ <string>NO</string>
+ <key>Fuzziness</key>
+ <real>0.0</real>
+ <key>ShadowVector</key>
+ <string>{0, 2}</string>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>Color</key>
+ <dict>
+ <key>b</key>
+ <string>0</string>
+ <key>g</key>
+ <string>0.501961</string>
+ <key>r</key>
+ <string>0</string>
+ </dict>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;\red0\green128\blue0;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf2 process_request}</string>
+ </dict>
+ <key>TextRelativeArea</key>
+ <string>{{0.125, 0.25}, {0.75, 0.5}}</string>
+ <key>isConnectedShape</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{360, 63}, {144, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>12</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 HttpResponse}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{72, 63}, {144, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>11</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 HttpRequest}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{72, 324}, {432, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>10</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 MessageMiddleware}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{72, 279}, {432, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>9</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 AuthenticationMiddleware}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{72, 234}, {432, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>8</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 CsrfViewMiddleware}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{72, 189}, {432, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>7</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 SessionMiddleware}</string>
+ </dict>
+ </dict>
+ <dict>
+ <key>Bounds</key>
+ <string>{{72, 144}, {432, 27}}</string>
+ <key>Class</key>
+ <string>ShapedGraphic</string>
+ <key>ID</key>
+ <integer>6</integer>
+ <key>Magnets</key>
+ <array>
+ <string>{0, 1}</string>
+ <string>{0, -1}</string>
+ <string>{1, 0}</string>
+ <string>{-1, 0}</string>
+ </array>
+ <key>Shape</key>
+ <string>Rectangle</string>
+ <key>Style</key>
+ <dict>
+ <key>fill</key>
+ <dict>
+ <key>FillType</key>
+ <integer>2</integer>
+ <key>GradientAngle</key>
+ <real>90</real>
+ <key>GradientColor</key>
+ <dict>
+ <key>w</key>
+ <string>0.666667</string>
+ </dict>
+ </dict>
+ <key>stroke</key>
+ <dict>
+ <key>CornerRadius</key>
+ <real>5</real>
+ </dict>
+ </dict>
+ <key>Text</key>
+ <dict>
+ <key>Text</key>
+ <string>{\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340
+\cocoascreenfonts1{\fonttbl\f0\fmodern\fcharset0 Courier;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc
+
+\f0\fs24 \cf0 CommonMiddleware}</string>
+ </dict>
+ </dict>
+ </array>
+ <key>GridInfo</key>
+ <dict>
+ <key>ShowsGrid</key>
+ <string>YES</string>
+ <key>SnapsToGrid</key>
+ <string>YES</string>
+ </dict>
+ <key>GuidesLocked</key>
+ <string>NO</string>
+ <key>GuidesVisible</key>
+ <string>YES</string>
+ <key>HPages</key>
+ <integer>1</integer>
+ <key>ImageCounter</key>
+ <integer>1</integer>
+ <key>KeepToScale</key>
+ <false/>
+ <key>Layers</key>
+ <array>
+ <dict>
+ <key>Lock</key>
+ <string>NO</string>
+ <key>Name</key>
+ <string>Calque 1</string>
+ <key>Print</key>
+ <string>YES</string>
+ <key>View</key>
+ <string>YES</string>
+ </dict>
+ </array>
+ <key>LayoutInfo</key>
+ <dict>
+ <key>Animate</key>
+ <string>NO</string>
+ <key>circoMinDist</key>
+ <real>18</real>
+ <key>circoSeparation</key>
+ <real>0.0</real>
+ <key>layoutEngine</key>
+ <string>dot</string>
+ <key>neatoSeparation</key>
+ <real>0.0</real>
+ <key>twopiSeparation</key>
+ <real>0.0</real>
+ </dict>
+ <key>LinksVisible</key>
+ <string>NO</string>
+ <key>MagnetsVisible</key>
+ <string>NO</string>
+ <key>MasterSheets</key>
+ <array/>
+ <key>ModificationDate</key>
+ <string>2012-12-09 19:48:54 +0000</string>
+ <key>Modifier</key>
+ <string>Aymeric Augustin</string>
+ <key>NotesVisible</key>
+ <string>NO</string>
+ <key>Orientation</key>
+ <integer>2</integer>
+ <key>OriginVisible</key>
+ <string>NO</string>
+ <key>PageBreaks</key>
+ <string>YES</string>
+ <key>PrintInfo</key>
+ <dict>
+ <key>NSBottomMargin</key>
+ <array>
+ <string>float</string>
+ <string>41</string>
+ </array>
+ <key>NSHorizonalPagination</key>
+ <array>
+ <string>coded</string>
+ <string>BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG</string>
+ </array>
+ <key>NSLeftMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSPaperSize</key>
+ <array>
+ <string>size</string>
+ <string>{595.28997802734375, 841.8900146484375}</string>
+ </array>
+ <key>NSPrintReverseOrientation</key>
+ <array>
+ <string>int</string>
+ <string>0</string>
+ </array>
+ <key>NSRightMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ <key>NSTopMargin</key>
+ <array>
+ <string>float</string>
+ <string>18</string>
+ </array>
+ </dict>
+ <key>PrintOnePage</key>
+ <false/>
+ <key>ReadOnly</key>
+ <string>NO</string>
+ <key>RowAlign</key>
+ <integer>1</integer>
+ <key>RowSpacing</key>
+ <real>36</real>
+ <key>SheetTitle</key>
+ <string>Canevas 1</string>
+ <key>SmartAlignmentGuidesActive</key>
+ <string>YES</string>
+ <key>SmartDistanceGuidesActive</key>
+ <string>YES</string>
+ <key>UniqueID</key>
+ <integer>1</integer>
+ <key>UseEntirePage</key>
+ <false/>
+ <key>VPages</key>
+ <integer>1</integer>
+ <key>WindowInfo</key>
+ <dict>
+ <key>CurrentSheet</key>
+ <integer>0</integer>
+ <key>ExpandedCanvases</key>
+ <array/>
+ <key>Frame</key>
+ <string>{{248, 4}, {694, 874}}</string>
+ <key>ListView</key>
+ <true/>
+ <key>OutlineWidth</key>
+ <integer>142</integer>
+ <key>RightSidebar</key>
+ <false/>
+ <key>ShowRuler</key>
+ <true/>
+ <key>Sidebar</key>
+ <true/>
+ <key>SidebarWidth</key>
+ <integer>120</integer>
+ <key>VisibleRegion</key>
+ <string>{{0, 0}, {559, 735}}</string>
+ <key>Zoom</key>
+ <real>1</real>
+ <key>ZoomValues</key>
+ <array>
+ <array>
+ <string>Canevas 1</string>
+ <real>1</real>
+ <real>1</real>
+ </array>
+ </array>
+ </dict>
+</dict>
+</plist>
diff --git a/docs/topics/http/_images/middleware.pdf b/docs/topics/http/_images/middleware.pdf
new file mode 100644
index 0000000000..8a9e61ef40
--- /dev/null
+++ b/docs/topics/http/_images/middleware.pdf
Binary files differ
diff --git a/docs/topics/http/_images/middleware.png b/docs/topics/http/_images/middleware.png
deleted file mode 100644
index 505c70ac36..0000000000
--- a/docs/topics/http/_images/middleware.png
+++ /dev/null
Binary files differ
diff --git a/docs/topics/http/_images/middleware.svg b/docs/topics/http/_images/middleware.svg
new file mode 100644
index 0000000000..4da8d22da8
--- /dev/null
+++ b/docs/topics/http/_images/middleware.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="52 47 481 409" width="481pt" height="409pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-12-09 19:48Z</dc:date><!-- Produced by OmniGraffle Professional 5.4.2 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><linearGradient x1="0" x2="1" id="Gradient" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="white"/><stop offset="1" stop-color="#aaa"/></linearGradient><linearGradient id="Obj_Gradient" xl:href="#Gradient" gradientTransform="translate(288 144) rotate(90) scale(27)"/><font-face font-family="Courier" font-size="12" units-per-em="1000" underline-position="-178.22266" underline-thickness="57.617188" slope="0" x-height="462.40234" cap-height="594.72656" ascent="753.90625" descent="-246.09375" font-weight="500"><font-face-src><font-face-name name="Courier"/></font-face-src></font-face><linearGradient id="Obj_Gradient_2" xl:href="#Gradient" gradientTransform="translate(288 189) rotate(90) scale(27)"/><linearGradient id="Obj_Gradient_3" xl:href="#Gradient" gradientTransform="translate(288 234) rotate(90) scale(27)"/><linearGradient id="Obj_Gradient_4" xl:href="#Gradient" gradientTransform="translate(288 279) rotate(90) scale(27)"/><linearGradient id="Obj_Gradient_5" xl:href="#Gradient" gradientTransform="translate(288 324) rotate(90) scale(27)"/><font-face font-family="Helvetica" font-size="12" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canevas 1</title><rect fill="white" width="559.28998" height="782.89"/><g><title>Calque 1</title><g><use xl:href="#id6_Graphic" filter="url(#Shadow)"/><use xl:href="#id7_Graphic" filter="url(#Shadow)"/><use xl:href="#id8_Graphic" filter="url(#Shadow)"/><use xl:href="#id9_Graphic" filter="url(#Shadow)"/><use xl:href="#id10_Graphic" filter="url(#Shadow)"/><use xl:href="#id11_Graphic" filter="url(#Shadow)"/><use xl:href="#id12_Graphic" filter="url(#Shadow)"/><use xl:href="#id33_Graphic" filter="url(#Shadow)"/></g><g id="id6_Graphic"><path d="M 77 144 L 499 144 C 501.76142 144 504 146.23858 504 149 L 504 166 C 504 168.76142 501.76142 171 499 171 L 77 171 C 74.238576 171 72 168.76142 72 166 C 72 166 72 166 72 166 L 72 149 C 72 146.23858 74.238576 144 77 144 C 77 144 77 144 77 144 Z" fill="url(#Obj_Gradient)"/><path d="M 77 144 L 499 144 C 501.76142 144 504 146.23858 504 149 L 504 166 C 504 168.76142 501.76142 171 499 171 L 77 171 C 74.238576 171 72 168.76142 72 166 C 72 166 72 166 72 166 L 72 149 C 72 146.23858 74.238576 144 77 144 C 77 144 77 144 77 144 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(77 150.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" fill="black" x="153.390625" y="11" textLength="115.21875">CommonMiddleware</tspan></text></g><g id="id7_Graphic"><path d="M 77 189 L 499 189 C 501.76142 189 504 191.23858 504 194 L 504 211 C 504 213.76142 501.76142 216 499 216 L 77 216 C 74.238576 216 72 213.76142 72 211 C 72 211 72 211 72 211 L 72 194 C 72 191.23858 74.238576 189 77 189 C 77 189 77 189 77 189 Z" fill="url(#Obj_Gradient_2)"/><path d="M 77 189 L 499 189 C 501.76142 189 504 191.23858 504 194 L 504 211 C 504 213.76142 501.76142 216 499 216 L 77 216 C 74.238576 216 72 213.76142 72 211 C 72 211 72 211 72 211 L 72 194 C 72 191.23858 74.238576 189 77 189 C 77 189 77 189 77 189 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(77 195.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="149.79004" y="11" textLength="122.41992">SessionMiddleware</tspan></text></g><g id="id8_Graphic"><path d="M 77 234 L 499 234 C 501.76142 234 504 236.23858 504 239 L 504 256 C 504 258.76142 501.76142 261 499 261 L 77 261 C 74.238576 261 72 258.76142 72 256 C 72 256 72 256 72 256 L 72 239 C 72 236.23858 74.238576 234 77 234 C 77 234 77 234 77 234 Z" fill="url(#Obj_Gradient_3)"/><path d="M 77 234 L 499 234 C 501.76142 234 504 236.23858 504 239 L 504 256 C 504 258.76142 501.76142 261 499 261 L 77 261 C 74.238576 261 72 258.76142 72 256 C 72 256 72 256 72 256 L 72 239 C 72 236.23858 74.238576 234 77 234 C 77 234 77 234 77 234 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(77 240.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="146.18945" y="11" textLength="129.62109">CsrfViewMiddleware</tspan></text></g><g id="id9_Graphic"><path d="M 77 279 L 499 279 C 501.76142 279 504 281.23858 504 284 L 504 301 C 504 303.76142 501.76142 306 499 306 L 77 306 C 74.238576 306 72 303.76142 72 301 C 72 301 72 301 72 301 L 72 284 C 72 281.23858 74.238576 279 77 279 C 77 279 77 279 77 279 Z" fill="url(#Obj_Gradient_4)"/><path d="M 77 279 L 499 279 C 501.76142 279 504 281.23858 504 284 L 504 301 C 504 303.76142 501.76142 306 499 306 L 77 306 C 74.238576 306 72 303.76142 72 301 C 72 301 72 301 72 301 L 72 284 C 72 281.23858 74.238576 279 77 279 C 77 279 77 279 77 279 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(77 285.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="124.58594" y="11" textLength="172.82812">AuthenticationMiddleware</tspan></text></g><g id="id10_Graphic"><path d="M 77 324 L 499 324 C 501.76142 324 504 326.23858 504 329 L 504 346 C 504 348.76142 501.76142 351 499 351 L 77 351 C 74.238576 351 72 348.76142 72 346 C 72 346 72 346 72 346 L 72 329 C 72 326.23858 74.238576 324 77 324 C 77 324 77 324 77 324 Z" fill="url(#Obj_Gradient_5)"/><path d="M 77 324 L 499 324 C 501.76142 324 504 326.23858 504 329 L 504 346 C 504 348.76142 501.76142 351 499 351 L 77 351 C 74.238576 351 72 348.76142 72 346 C 72 346 72 346 72 346 L 72 329 C 72 326.23858 74.238576 324 77 324 C 77 324 77 324 77 324 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(77 330.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="149.79004" y="11" textLength="122.41992">MessageMiddleware</tspan></text></g><g id="id11_Graphic"><rect x="72" y="63" width="144" height="27" fill="white"/><rect x="72" y="63" width="144" height="27" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(77 69.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="27.393555" y="11" textLength="79.21289">HttpRequest</tspan></text></g><g id="id12_Graphic"><rect x="360" y="63" width="144" height="27" fill="white"/><rect x="360" y="63" width="144" height="27" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(365 69.5)" fill="black"><tspan font-family="Courier" font-size="12" font-weight="500" x="23.792969" y="11" textLength="86.41406">HttpResponse</tspan></text></g><path d="M 99 90 L 117 90 L 117 360 L 126 360 L 108 387 L 90 360 L 99 360 Z" fill="white" fill-opacity=".8"/><path d="M 99 90 L 117 90 L 117 360 L 126 360 L 108 387 L 90 360 L 99 360 Z" stroke="green" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(115 132.125) rotate(90)" fill="green"><tspan font-family="Courier" font-size="12" font-weight="500" fill="green" x="52.36621" y="11" textLength="108.01758">process_request</tspan></text><path d="M 162 117 L 180 117 L 180 378 L 189 378 L 171 405 L 153 378 L 162 378 Z" fill="white" fill-opacity=".8"/><path d="M 162 117 L 180 117 L 180 378 L 189 378 L 171 405 L 153 378 L 162 378 Z" stroke="green" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(178 158) rotate(90)" fill="green"><tspan font-family="Courier" font-size="12" font-weight="500" fill="green" x="59.79297" y="11" textLength="86.41406">process_view</tspan></text><path d="M 441 405 L 423 405 L 423 135 L 414 135 L 432 108 L 450 135 L 441 135 Z" fill="white" fill-opacity=".8"/><path d="M 441 405 L 423 405 L 423 135 L 414 135 L 432 108 L 450 135 L 441 135 Z" stroke="green" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/><text transform="translate(425 362.875) rotate(-90)" fill="green"><tspan font-family="Courier" font-size="12" font-weight="500" fill="green" x="16.360352" y="11" textLength="180.0293">process_template_response</tspan></text><path d="M 495 405 L 477 405 L 477 117 L 468 117 L 486 90 L 504 117 L 495 117 Z" fill="white" fill-opacity=".8"/><path d="M 495 405 L 477 405 L 477 117 L 468 117 L 486 90 L 504 117 L 495 117 Z" stroke="green" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(479 360.625) rotate(-90)" fill="green"><tspan font-family="Courier" font-size="12" font-weight="500" fill="green" x="55.515625" y="11" textLength="115.21875">process_response</tspan></text><path d="M 387 405 L 369 405 L 369 135 L 360 135 L 378 108 L 396 135 L 387 135 Z" fill="white" fill-opacity=".8"/><path d="M 387 405 L 369 405 L 369 135 L 360 135 L 378 108 L 396 135 L 387 135 Z" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="4,4"/><text transform="translate(371 362.875) rotate(-90)" fill="red"><tspan font-family="Courier" font-size="12" font-weight="500" fill="red" x="45.16504" y="11" textLength="122.41992">process_exception</tspan></text><g id="id33_Graphic"><path d="M 144 405 L 505.4694 405 L 513 418.5 L 505.4694 432 L 144 432 L 153 418.5 Z" fill="white"/><path d="M 144 405 L 505.4694 405 L 513 418.5 L 505.4694 432 L 144 432 L 153 418.5 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(149 411.5)" fill="black"><tspan font-family="Helvetica" font-size="12" font-weight="500" fill="black" x="144.81543" y="11" textLength="69.36914">view function</tspan></text></g></g></g></svg>
diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt
index c27e7e8690..18243c77ce 100644
--- a/docs/topics/http/middleware.txt
+++ b/docs/topics/http/middleware.txt
@@ -4,25 +4,28 @@ Middleware
Middleware is a framework of hooks into Django's request/response processing.
It's a light, low-level "plugin" system for globally altering Django's input
-and/or output.
+or output.
Each middleware component is responsible for doing some specific function. For
-example, Django includes a middleware component, ``XViewMiddleware``, that adds
-an ``"X-View"`` HTTP header to every response to a ``HEAD`` request.
+example, Django includes a middleware component,
+:class:`~django.middleware.transaction.TransactionMiddleware`, that wraps the
+processing of each HTTP request in a database transaction.
This document explains how middleware works, how you activate middleware, and
how to write your own middleware. Django ships with some built-in middleware
-you can use right out of the box; they're documented in the :doc:`built-in
+you can use right out of the box. They're documented in the :doc:`built-in
middleware reference </ref/middleware>`.
Activating middleware
=====================
-To activate a middleware component, add it to the :setting:`MIDDLEWARE_CLASSES`
-list in your Django settings. In :setting:`MIDDLEWARE_CLASSES`, each middleware
-component is represented by a string: the full Python path to the middleware's
-class name. For example, here's the default :setting:`MIDDLEWARE_CLASSES`
-created by :djadmin:`django-admin.py startproject <startproject>`::
+To activate a middleware component, add it to the
+:setting:`MIDDLEWARE_CLASSES` tuple in your Django settings.
+
+In :setting:`MIDDLEWARE_CLASSES`, each middleware component is represented by
+a string: the full Python path to the middleware's class name. For example,
+here's the default value created by :djadmin:`django-admin.py startproject
+<startproject>`::
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
@@ -32,23 +35,44 @@ created by :djadmin:`django-admin.py startproject <startproject>`::
'django.contrib.messages.middleware.MessageMiddleware',
)
-During the request phases (:meth:`process_request` and :meth:`process_view`
-middleware), Django applies middleware in the order it's defined in
-:setting:`MIDDLEWARE_CLASSES`, top-down. During the response phases
-(:meth:`process_response` and :meth:`process_exception` middleware), the
-classes are applied in reverse order, from the bottom up. You can think of it
-like an onion: each middleware class is a "layer" that wraps the view:
-
-.. image:: _images/middleware.png
- :width: 502
- :height: 417
- :alt: Middleware application order.
-
-A Django installation doesn't require any middleware -- e.g.,
-:setting:`MIDDLEWARE_CLASSES` can be empty, if you'd like -- but it's strongly
+A Django installation doesn't require any middleware —
+:setting:`MIDDLEWARE_CLASSES` can be empty, if you'd like — but it's strongly
suggested that you at least use
:class:`~django.middleware.common.CommonMiddleware`.
+The order in :setting:`MIDDLEWARE_CLASSES` matters because a middleware can
+depend on other middleware. For instance,
+:class:`~django.contrib.auth.middleware.AuthenticationMiddleware` stores the
+authenticated user in the session; therefore, it must run after
+:class:`~django.contrib.sessions.middleware.SessionMiddleware`.
+
+Hooks and application order
+===========================
+
+During the request phase, before calling the view, Django applies middleware
+in the order it's defined in :setting:`MIDDLEWARE_CLASSES`, top-down. Two
+hooks are available:
+
+* :meth:`process_request`
+* :meth:`process_view`
+
+During the response phase, after calling the view, middleware are applied in
+reverse order, from the bottom up. Three hooks are available:
+
+* :meth:`process_exception` (only if the view raised an exception)
+* :meth:`process_template_response` (only for template responses)
+* :meth:`process_response`
+
+.. image:: _images/middleware.*
+ :alt: middleware application order
+ :width: 481
+ :height: 409
+
+If you prefer, you can also think of it like an onion: each middleware class
+is a "layer" that wraps the view.
+
+The behavior of each hook is described below.
+
Writing your own middleware
===========================
@@ -62,16 +86,19 @@ Python class that defines one or more of the following methods:
.. method:: process_request(self, request)
-``request`` is an :class:`~django.http.HttpRequest` object. This method is
-called on each request, before Django decides which view to execute.
+``request`` is an :class:`~django.http.HttpRequest` object.
+
+``process_request()`` is called on each request, before Django decides which
+view to execute.
-``process_request()`` should return either ``None`` or an
-:class:`~django.http.HttpResponse` object. If it returns ``None``, Django will
-continue processing this request, executing any other middleware and, then, the
-appropriate view. If it returns an :class:`~django.http.HttpResponse` object,
-Django won't bother calling ANY other request, view or exception middleware, or
-the appropriate view; it'll return that :class:`~django.http.HttpResponse`.
-Response middleware is always called on every response.
+It should return either ``None`` or an :class:`~django.http.HttpResponse`
+object. If it returns ``None``, Django will continue processing this request,
+executing any other ``process_request()`` middleware, then, ``process_view()``
+middleware, and finally, the appropriate view. If it returns an
+:class:`~django.http.HttpResponse` object, Django won't bother calling any
+other request, view or exception middleware, or the appropriate view; it'll
+apply response middleware to that :class:`~django.http.HttpResponse`, and
+return the result.
.. _view-middleware:
@@ -88,14 +115,15 @@ dictionary of keyword arguments that will be passed to the view. Neither
``view_args`` nor ``view_kwargs`` include the first view argument
(``request``).
-``process_view()`` is called just before Django calls the view. It should
-return either ``None`` or an :class:`~django.http.HttpResponse` object. If it
-returns ``None``, Django will continue processing this request, executing any
-other ``process_view()`` middleware and, then, the appropriate view. If it
-returns an :class:`~django.http.HttpResponse` object, Django won't bother
-calling ANY other request, view or exception middleware, or the appropriate
-view; it'll return that :class:`~django.http.HttpResponse`. Response
-middleware is always called on every response.
+``process_view()`` is called just before Django calls the view.
+
+It should return either ``None`` or an :class:`~django.http.HttpResponse`
+object. If it returns ``None``, Django will continue processing this request,
+executing any other ``process_view()`` middleware and, then, the appropriate
+view. If it returns an :class:`~django.http.HttpResponse` object, Django won't
+bother calling any other view or exception middleware, or the appropriate
+view; it'll apply response middleware to that
+:class:`~django.http.HttpResponse`, and return the result.
.. note::
@@ -119,19 +147,17 @@ middleware is always called on every response.
.. method:: process_template_response(self, request, response)
-``request`` is an :class:`~django.http.HttpRequest` object. ``response`` is a
-subclass of :class:`~django.template.response.SimpleTemplateResponse` (e.g.
-:class:`~django.template.response.TemplateResponse`) or any response object
-that implements a ``render`` method.
+``request`` is an :class:`~django.http.HttpRequest` object. ``response`` is
+the :class:`~django.template.response.TemplateResponse` object (or equivalent)
+returned by a Django view or by a middleware.
-``process_template_response()`` must return a response object that implements a
-``render`` method. It could alter the given ``response`` by changing
-``response.template_name`` and ``response.context_data``, or it could create
-and return a brand-new
-:class:`~django.template.response.SimpleTemplateResponse` or equivalent.
+``process_template_response()`` is called just after the view has finished
+executing, if the response instance has a ``render()`` method, indicating that
+it is a :class:`~django.template.response.TemplateResponse` or equivalent.
-``process_template_response()`` will only be called if the response
-instance has a ``render()`` method, indicating that it is a
+It must return a response object that implements a ``render`` method. It could
+alter the given ``response`` by changing ``response.template_name`` and
+``response.context_data``, or it could create and return a brand-new
:class:`~django.template.response.TemplateResponse` or equivalent.
You don't need to explicitly render responses -- responses will be
@@ -139,7 +165,7 @@ automatically rendered once all template response middleware has been
called.
Middleware are run in reverse order during the response phase, which
-includes process_template_response.
+includes ``process_template_response()``.
.. _response-middleware:
@@ -148,21 +174,34 @@ includes process_template_response.
.. method:: process_response(self, request, response)
-``request`` is an :class:`~django.http.HttpRequest` object. ``response`` is the
-:class:`~django.http.HttpResponse` object returned by a Django view.
+``request`` is an :class:`~django.http.HttpRequest` object. ``response`` is
+the :class:`~django.http.HttpResponse` or
+:class:`~django.http.StreamingHttpResponse` object returned by a Django view
+or by a middleware.
+
+``process_response()`` is called on all responses before they're returned to
+the browser.
-``process_response()`` must return an :class:`~django.http.HttpResponse`
-object. It could alter the given ``response``, or it could create and return a
-brand-new :class:`~django.http.HttpResponse`.
+It must return an :class:`~django.http.HttpResponse` or
+:class:`~django.http.StreamingHttpResponse` object. It could alter the given
+``response``, or it could create and return a brand-new
+:class:`~django.http.HttpResponse` or
+:class:`~django.http.StreamingHttpResponse`.
Unlike the ``process_request()`` and ``process_view()`` methods, the
-``process_response()`` method is always called, even if the ``process_request()``
-and ``process_view()`` methods of the same middleware class were skipped because
-an earlier middleware method returned an :class:`~django.http.HttpResponse`
-(this means that your ``process_response()`` method cannot rely on setup done in
-``process_request()``, for example). In addition, during the response phase the
-classes are applied in reverse order, from the bottom up. This means classes
-defined at the end of :setting:`MIDDLEWARE_CLASSES` will be run first.
+``process_response()`` method is always called, even if the
+``process_request()`` and ``process_view()`` methods of the same middleware
+class were skipped (because an earlier middleware method returned an
+:class:`~django.http.HttpResponse`). In particular, this means that your
+``process_response()`` method cannot rely on setup done in
+``process_request()``.
+
+Finally, remember that during the response phase, middleware are applied in
+reverse order, from the bottom up. This means classes defined at the end of
+:setting:`MIDDLEWARE_CLASSES` will be run first.
+
+Dealing with streaming responses
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionchanged:: 1.5
``response`` may also be an :class:`~django.http.StreamingHttpResponse`
@@ -177,10 +216,17 @@ must test for streaming responses and adjust their behavior accordingly::
if response.streaming:
response.streaming_content = wrap_streaming_content(response.streaming_content)
else:
- response.content = wrap_content(response.content)
+ response.content = alter_content(response.content)
+
+.. note::
+
+ ``streaming_content`` should be assumed to be too large to hold in memory.
+ Response middleware may wrap it in a new generator, but must not consume
+ it. Wrapping is typically implemented as follows::
-``streaming_content`` should be assumed to be too large to hold in memory.
-Middleware may wrap it in a new generator, but must not consume it.
+ def wrap_streaming_content(content)
+ for chunk in content:
+ yield alter_content(chunk)
.. _exception-middleware:
@@ -195,8 +241,9 @@ Middleware may wrap it in a new generator, but must not consume it.
Django calls ``process_exception()`` when a view raises an exception.
``process_exception()`` should return either ``None`` or an
:class:`~django.http.HttpResponse` object. If it returns an
-:class:`~django.http.HttpResponse` object, the response will be returned to
-the browser. Otherwise, default exception handling kicks in.
+:class:`~django.http.HttpResponse` object, the template response and response
+middleware will be applied, and the resulting response returned to the
+browser. Otherwise, default exception handling kicks in.
Again, middleware are run in reverse order during the response phase, which
includes ``process_exception``. If an exception middleware returns a response,
@@ -221,9 +268,9 @@ Marking middleware as unused
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's sometimes useful to determine at run-time whether a piece of middleware
-should be used. In these cases, your middleware's ``__init__`` method may raise
-``django.core.exceptions.MiddlewareNotUsed``. Django will then remove that
-piece of middleware from the middleware process.
+should be used. In these cases, your middleware's ``__init__`` method may
+raise :exc:`django.core.exceptions.MiddlewareNotUsed`. Django will then remove
+that piece of middleware from the middleware process.
Guidelines
----------
diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt
index 15f9f7feba..baf8aa5cb5 100644
--- a/docs/topics/http/sessions.txt
+++ b/docs/topics/http/sessions.txt
@@ -45,6 +45,8 @@ If you want to use a database-backed session, you need to add
Once you have configured your installation, run ``manage.py syncdb``
to install the single database table that stores session data.
+.. _cached-sessions-backend:
+
Using cached sessions
---------------------
@@ -62,6 +64,13 @@ sure you've configured your cache; see the :doc:`cache documentation
sessions directly instead of sending everything through the file or
database cache backends.
+If you have multiple caches defined in :setting:`CACHES`, Django will use the
+default cache. To use another cache, set :setting:`SESSION_CACHE_ALIAS` to the
+name of that cache.
+
+.. versionchanged:: 1.5
+ The :setting:`SESSION_CACHE_ALIAS` setting was added.
+
Once your cache is configured, you've got two choices for how to store data in
the cache:
@@ -250,17 +259,35 @@ You can edit it multiple times.
with no custom expiration (or those set to expire at browser close), this
will equal :setting:`SESSION_COOKIE_AGE`.
+ This function accepts two optional keyword arguments:
+
+ - ``modification``: last modification of the session, as a
+ :class:`~datetime.datetime` object. Defaults to the current time.
+ - ``expiry``: expiry information for the session, as a
+ :class:`~datetime.datetime` object, an :class:`int` (in seconds), or
+ ``None``. Defaults to the value stored in the session by
+ :meth:`set_expiry`, if there is one, or ``None``.
+
.. method:: get_expiry_date
Returns the date this session will expire. For sessions with no custom
expiration (or those set to expire at browser close), this will equal the
date :setting:`SESSION_COOKIE_AGE` seconds from now.
+ This function accepts the same keyword argumets as :meth:`get_expiry_age`.
+
.. method:: get_expire_at_browser_close
Returns either ``True`` or ``False``, depending on whether the user's
session cookie will expire when the user's Web browser is closed.
+ .. method:: SessionBase.clear_expired
+
+ .. versionadded:: 1.5
+
+ Removes expired sessions from the session store. This class method is
+ called by :djadmin:`clearsessions`.
+
Session object guidelines
-------------------------
@@ -447,22 +474,29 @@ This setting is a global default and can be overwritten at a per-session level
by explicitly calling the :meth:`~backends.base.SessionBase.set_expiry` method
of ``request.session`` as described above in `using sessions in views`_.
-Clearing the session table
+Clearing the session store
==========================
-If you're using the database backend, note that session data can accumulate in
-the ``django_session`` database table and Django does *not* provide automatic
-purging. Therefore, it's your job to purge expired sessions on a regular basis.
+As users create new sessions on your website, session data can accumulate in
+your session store. If you're using the database backend, the
+``django_session`` database table will grow. If you're using the file backend,
+your temporary directory will contain an increasing number of files.
-To understand this problem, consider what happens when a user uses a session.
+To understand this problem, consider what happens with the database backend.
When a user logs in, Django adds a row to the ``django_session`` database
table. Django updates this row each time the session data changes. If the user
logs out manually, Django deletes the row. But if the user does *not* log out,
-the row never gets deleted.
+the row never gets deleted. A similar process happens with the file backend.
+
+Django does *not* provide automatic purging of expired sessions. Therefore,
+it's your job to purge expired sessions on a regular basis. Django provides a
+clean-up management command for this purpose: :djadmin:`clearsessions`. It's
+recommended to call this command on a regular basis, for example as a daily
+cron job.
-Django provides a sample clean-up script: ``django-admin.py cleanup``.
-That script deletes any session in the session table whose ``expire_date`` is
-in the past -- but your application may have different requirements.
+Note that the cache backend isn't vulnerable to this problem, because caches
+automatically delete stale data. Neither is the cookie backend, because the
+session data is stored by the users' browsers.
Settings
========
diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt
index 0dc38b1459..b1b4700b73 100644
--- a/docs/topics/http/shortcuts.txt
+++ b/docs/topics/http/shortcuts.txt
@@ -4,7 +4,7 @@ Django shortcut functions
.. module:: django.shortcuts
:synopsis:
- Convenience shortcuts that spam multiple levels of Django's MVC stack.
+ Convenience shortcuts that span multiple levels of Django's MVC stack.
.. index:: shortcuts
diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt
index e178df2af2..00c07da6ea 100644
--- a/docs/topics/http/urls.txt
+++ b/docs/topics/http/urls.txt
@@ -552,12 +552,11 @@ layers where URLs are needed:
* In templates: Using the :ttag:`url` template tag.
-* In Python code: Using the :func:`django.core.urlresolvers.reverse()`
+* In Python code: Using the :func:`django.core.urlresolvers.reverse`
function.
* In higher level code related to handling of URLs of Django model instances:
- The :meth:`django.db.models.Model.get_absolute_url()` method and the
- :func:`django.db.models.permalink` decorator.
+ The :meth:`~django.db.models.Model.get_absolute_url` method.
Examples
--------
@@ -622,10 +621,10 @@ view::
)
This is completely valid, but it leads to problems when you try to do reverse
-URL matching (through the :func:`~django.db.models.permalink` decorator or the
-:ttag:`url` template tag). Continuing this example, if you wanted to retrieve
-the URL for the ``archive`` view, Django's reverse URL matcher would get
-confused, because *two* URL patterns point at that view.
+URL matching (through the :func:`~django.core.urlresolvers.reverse` function
+or the :ttag:`url` template tag). Continuing this example, if you wanted to
+retrieve the URL for the ``archive`` view, Django's reverse URL matcher would
+get confused, because *two* URL patterns point at that view.
To solve this problem, Django supports **named URL patterns**. That is, you can
give a name to a URL pattern in order to distinguish it from other patterns
@@ -724,7 +723,7 @@ the fully qualified name into parts, and then tries the following lookup:
render a template.
The current application can also be specified manually as an argument
- to the :func:`django.core.urlresolvers.reverse()` function.
+ to the :func:`django.core.urlresolvers.reverse` function.
3. If there is no current application. Django looks for a default
application instance. The default application instance is the instance
diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt
index 7c4d1bbb6e..caa2882f37 100644
--- a/docs/topics/http/views.txt
+++ b/docs/topics/http/views.txt
@@ -209,6 +209,17 @@ This view loads and renders the template ``403.html`` in your root template
directory, or if this file does not exist, instead serves the text
"403 Forbidden", as per :rfc:`2616` (the HTTP 1.1 Specification).
+``django.views.defaults.permission_denied`` is triggered by a
+:exc:`~django.core.exceptions.PermissionDenied` exception. To deny access in a
+view you can use code like this::
+
+ from django.core.exceptions import PermissionDenied
+
+ def edit(request, pk):
+ if not request.user.is_staff:
+ raise PermissionDenied
+ # ...
+
It is possible to override ``django.views.defaults.permission_denied`` in the
same way you can for the 404 and 500 views by specifying a ``handler403`` in
your URLconf::
diff --git a/docs/topics/install.txt b/docs/topics/install.txt
index 52994ed16a..b71033f319 100644
--- a/docs/topics/install.txt
+++ b/docs/topics/install.txt
@@ -10,7 +10,7 @@ Install Python
Being a Python Web framework, Django requires Python.
It works with any Python version from 2.6.5 to 2.7. It also features
-experimental support for versions 3.2 and 3.3.
+experimental support for versions from 3.2.3 to 3.3.
Get Python at http://www.python.org. If you're running Linux or Mac OS X, you
probably already have it installed.
@@ -106,15 +106,15 @@ support channels provided by each 3rd party project.
In addition to a database backend, you'll need to make sure your Python
database bindings are installed.
-* If you're using PostgreSQL, you'll need the ``postgresql_psycopg2`` package.
+* If you're using PostgreSQL, you'll need the `postgresql_psycopg2`_ package.
You might want to refer to our :ref:`PostgreSQL notes <postgresql-notes>` for
further technical details specific to this database.
If you're on Windows, check out the unofficial `compiled Windows version`_.
-* If you're using MySQL, you'll need the ``MySQL-python`` package, version 1.2.1p2 or higher. You
- will also want to read the database-specific :ref:`notes for the MySQL
- backend <mysql-notes>`.
+* If you're using MySQL, you'll need the ``MySQL-python`` package, version
+ 1.2.1p2 or higher. You will also want to read the database-specific
+ :ref:`notes for the MySQL backend <mysql-notes>`.
* If you're using Oracle, you'll need a copy of cx_Oracle_, but please
read the database-specific :ref:`notes for the Oracle backend <oracle-notes>`
@@ -124,21 +124,23 @@ database bindings are installed.
* If you're using an unofficial 3rd party backend, please consult the
documentation provided for any additional requirements.
-If you plan to use Django's ``manage.py syncdb`` command to
-automatically create database tables for your models, you'll need to
-ensure that Django has permission to create and alter tables in the
-database you're using; if you plan to manually create the tables, you
-can simply grant Django ``SELECT``, ``INSERT``, ``UPDATE`` and
-``DELETE`` permissions. On some databases, Django will need
-``ALTER TABLE`` privileges during ``syncdb`` but won't issue
-``ALTER TABLE`` statements on a table once ``syncdb`` has created it.
+If you plan to use Django's ``manage.py syncdb`` command to automatically
+create database tables for your models (after first installing Django and
+creating a project), you'll need to ensure that Django has permission to create
+and alter tables in the database you're using; if you plan to manually create
+the tables, you can simply grant Django ``SELECT``, ``INSERT``, ``UPDATE`` and
+``DELETE`` permissions. On some databases, Django will need ``ALTER TABLE``
+privileges during ``syncdb`` but won't issue ``ALTER TABLE`` statements on a
+table once ``syncdb`` has created it. After creating a database user with these
+permissions, you'll specify the details in your project's settings file,
+see :setting:`DATABASES` for details.
-If you're using Django's :doc:`testing framework</topics/testing>` to test database queries,
-Django will need permission to create a test database.
+If you're using Django's :doc:`testing framework</topics/testing>` to test
+database queries, Django will need permission to create a test database.
.. _PostgreSQL: http://www.postgresql.org/
.. _MySQL: http://www.mysql.com/
-.. _psycopg: http://initd.org/pub/software/psycopg/
+.. _postgresql_psycopg2: http://initd.org/psycopg/
.. _compiled Windows version: http://stickpeople.com/projects/python/win-psycopg/
.. _SQLite: http://www.sqlite.org/
.. _pysqlite: http://trac.edgewall.org/wiki/PySqlite
diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt
index 7bd56e92ec..652ad397ff 100644
--- a/docs/topics/logging.txt
+++ b/docs/topics/logging.txt
@@ -440,7 +440,7 @@ Handlers
Django provides one log handler in addition to those provided by the
Python logging module.
-.. class:: AdminEmailHandler([include_html=False])
+.. class:: AdminEmailHandler(include_html=False, email_backend=None)
This handler sends an email to the site admins for each log
message it receives.
@@ -470,13 +470,30 @@ Python logging module.
with names and values of local variables at each level of the stack, plus
the values of your Django settings. This information is potentially very
sensitive, and you may not want to send it over email. Consider using
- something such as `django-sentry`_ to get the best of both worlds -- the
+ something such as `Sentry`_ to get the best of both worlds -- the
rich information of full tracebacks plus the security of *not* sending the
information over email. You may also explicitly designate certain
sensitive information to be filtered out of error reports -- learn more on
:ref:`Filtering error reports<filtering-error-reports>`.
-.. _django-sentry: http://pypi.python.org/pypi/django-sentry
+ .. versionadded:: 1.6
+
+ By setting the ``email_backend`` argument of ``AdminEmailHandler``, the
+ :ref:`email backend <topic-email-backends>` that is being used by the
+ handler can be overridden, like this::
+
+ 'handlers': {
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'class': 'django.utils.log.AdminEmailHandler',
+ 'email_backend': 'django.core.mail.backends.filebased.EmailBackend',
+ }
+ },
+
+ By default, an instance of the email backend specified in
+ :setting:`EMAIL_BACKEND` will be used.
+
+.. _Sentry: http://pypi.python.org/pypi/sentry
Filters
diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt
index f5749faaf2..e6dc165399 100644
--- a/docs/topics/python3.txt
+++ b/docs/topics/python3.txt
@@ -278,15 +278,13 @@ Iterators
::
- class MyIterator(object):
+ class MyIterator(six.Iterator):
def __iter__(self):
return self # implement some logic here
def __next__(self):
raise StopIteration # implement some logic here
- next = __next__ # Python 2 compatibility
-
Boolean evaluation
~~~~~~~~~~~~~~~~~~
@@ -297,7 +295,8 @@ Boolean evaluation
def __bool__(self):
return True # implement some logic here
- __nonzero__ = __bool__ # Python 2 compatibility
+ def __nonzero__(self): # Python 2 compatibility
+ return type(self).__bool__(self)
Division
~~~~~~~~
@@ -309,12 +308,14 @@ Division
def __truediv__(self, other):
return self / other # implement some logic here
- __div__ = __truediv__ # Python 2 compatibility
+ def __div__(self, other): # Python 2 compatibility
+ return type(self).__truediv__(self, other)
def __itruediv__(self, other):
return self // other # implement some logic here
- __idiv__ = __itruediv__ # Python 2 compatibility
+ def __idiv__(self, other): # Python 2 compatibility
+ return type(self).__itruediv__(self, other)
.. module: django.utils.six
diff --git a/docs/topics/security.txt b/docs/topics/security.txt
index 0a3c6bff02..169f9ac773 100644
--- a/docs/topics/security.txt
+++ b/docs/topics/security.txt
@@ -185,6 +185,31 @@ recommend you ensure your Web server is configured such that:
Additionally, as of 1.3.1, Django requires you to explicitly enable support for
the ``X-Forwarded-Host`` header if your configuration requires it.
+Configuration for Apache
+------------------------
+
+The easiest way to get the described behavior in Apache is as follows. Create
+a `virtual host`_ using the ServerName_ and ServerAlias_ directives to restrict
+the domains Apache reacts to. Please keep in mind that while the directives do
+support ports the match is only performed against the hostname. This means that
+the ``Host`` header could still contain a port pointing to another webserver on
+the same machine. The next step is to make sure that your newly created virtual
+host is not also the default virtual host. Apache uses the first virtual host
+found in the configuration file as default virtual host. As such you have to
+ensure that you have another virtual host which will act as catch-all virtual
+host. Just add one if you do not have one already, there is nothing special
+about it aside from ensuring it is the first virtual host in the configuration
+file. Debian/Ubuntu users usually don't have to take any action, since Apache
+ships with a default virtual host in ``sites-available`` which is linked into
+``sites-enabled`` as ``000-default`` and included from ``apache2.conf``. Just
+make sure not to name your site ``000-abc``, since files are included in
+alphabetical order.
+
+.. _virtual host: http://httpd.apache.org/docs/2.2/vhosts/
+.. _ServerName: http://httpd.apache.org/docs/2.2/mod/core.html#servername
+.. _ServerAlias: http://httpd.apache.org/docs/2.2/mod/core.html#serveralias
+
+
.. _additional-security-topics:
Additional security topics
diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt
index 9b44166e42..28f600e223 100644
--- a/docs/topics/serialization.txt
+++ b/docs/topics/serialization.txt
@@ -166,15 +166,6 @@ Notes for specific serialization formats
json
^^^^
-If you're using UTF-8 (or any other non-ASCII encoding) data with the JSON
-serializer, you must pass ``ensure_ascii=False`` as a parameter to the
-``serialize()`` call. Otherwise, the output won't be encoded correctly.
-
-For example::
-
- json_serializer = serializers.get_serializer("json")()
- json_serializer.serialize(queryset, ensure_ascii=False, stream=response)
-
Be aware that not all Django output can be passed unmodified to :mod:`json`.
In particular, :ref:`lazy translation objects <lazy-translations>` need a
`special encoder`_ written for them. Something like this will work::
diff --git a/docs/topics/templates.txt b/docs/topics/templates.txt
index af45a8d95b..fb2119515b 100644
--- a/docs/topics/templates.txt
+++ b/docs/topics/templates.txt
@@ -16,9 +16,9 @@ or CheetahTemplate_, you should feel right at home with Django's templates.
.. admonition:: Philosophy
If you have a background in programming, or if you're used to languages
- like PHP which mix programming code directly into HTML, you'll want to
- bear in mind that the Django template system is not simply Python embedded
- into HTML. This is by design: the template system is meant to express
+ which mix programming code directly into HTML, you'll want to bear in
+ mind that the Django template system is not simply Python embedded into
+ HTML. This is by design: the template system is meant to express
presentation, not program logic.
The Django template system provides tags which function similarly to some
diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
index d0b2e7cdf9..8c11e32a55 100644
--- a/docs/topics/testing.txt
+++ b/docs/topics/testing.txt
@@ -379,6 +379,15 @@ control the particular collation used by the test database. See the
:doc:`settings documentation </ref/settings>` for details of these
advanced settings.
+.. admonition:: Finding data from your production database when running tests?
+
+ If your code attempts to access the database when its modules are compiled,
+ this will occur *before* the test database is set up, with potentially
+ unexpected results. For example, if you have a database query in
+ module-level code and a real database exists, production data could pollute
+ your tests. *It is a bad idea to have such import-time database queries in
+ your code* anyway - rewrite your code so that it doesn't do this.
+
.. _topics-testing-masterslave:
Testing master/slave configurations
@@ -1177,10 +1186,13 @@ Normal Python unit test classes extend a base class of
.. _testcase_hierarchy_diagram:
-.. figure:: _images/django_unittest_classes_hierarchy.png
+.. figure:: _images/django_unittest_classes_hierarchy.*
:alt: Hierarchy of Django unit testing classes (TestCase subclasses)
+ :width: 508
+ :height: 391
- Hierarchy of Django unit testing classes
+Regardless of the version of Python you're using, if you've installed
+:mod:`unittest2`, :mod:`django.utils.unittest` will point to that library.
TestCase
^^^^^^^^
@@ -1586,15 +1598,24 @@ The decorator can also be applied to test case classes::
the original ``LoginTestCase`` is still equally affected by the
decorator.
-.. note::
+When overriding settings, make sure to handle the cases in which your app's
+code uses a cache or similar feature that retains state even if the
+setting is changed. Django provides the
+:data:`django.test.signals.setting_changed` signal that lets you register
+callbacks to clean up and otherwise reset state when settings are changed.
+
+Django itself uses this signal to reset various data:
- When overriding settings, make sure to handle the cases in which your app's
- code uses a cache or similar feature that retains state even if the
- setting is changed. Django provides the
- :data:`django.test.signals.setting_changed` signal that lets you register
- callbacks to clean up and otherwise reset state when settings are changed.
- Note that this signal isn't currently used by Django itself, so changing
- built-in settings may not yield the results you expect.
+================================ ========================
+Overriden settings Data reset
+================================ ========================
+USE_TZ, TIME_ZONE Databases timezone
+TEMPLATE_CONTEXT_PROCESSORS Context processors cache
+TEMPLATE_LOADERS Template loaders cache
+SERIALIZATION_MODULES Serializers cache
+LOCALE_PATHS, LANGUAGE_CODE Default translation and loaded translations
+MEDIA_ROOT, DEFAULT_FILE_STORAGE Default file storage
+================================ ========================
Emptying the test outbox
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1752,6 +1773,11 @@ your test suite.
via an explicit ``order_by()`` call on the queryset prior to
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)
@@ -2075,6 +2101,7 @@ out the `full reference`_ for more details.
def test_login(self):
from selenium.webdriver.support.wait import WebDriverWait
+ timeout = 2
...
self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
# Wait until the response is received
diff --git a/tests/modeltests/basic/tests.py b/tests/modeltests/basic/tests.py
index ebd70d14d9..1c83b980a7 100644
--- a/tests/modeltests/basic/tests.py
+++ b/tests/modeltests/basic/tests.py
@@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals
from datetime import datetime
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db.models.fields import Field, FieldDoesNotExist
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
from django.utils import six
@@ -128,6 +128,40 @@ class ModelTest(TestCase):
b = Article.objects.get(pk=a.id)
self.assertEqual(a, b)
+ # Create a very similar object
+ a = Article(
+ id=None,
+ headline='Area man programs in Python',
+ pub_date=datetime(2005, 7, 28),
+ )
+ a.save()
+
+ self.assertEqual(Article.objects.count(), 2)
+
+ # Django raises an Article.MultipleObjectsReturned exception if the
+ # lookup matches more than one object
+ self.assertRaisesRegexp(
+ MultipleObjectsReturned,
+ "get\(\) returned more than one Article -- it returned 2!",
+ Article.objects.get,
+ headline__startswith='Area',
+ )
+
+ self.assertRaisesRegexp(
+ MultipleObjectsReturned,
+ "get\(\) returned more than one Article -- it returned 2!",
+ Article.objects.get,
+ pub_date__year=2005,
+ )
+
+ self.assertRaisesRegexp(
+ MultipleObjectsReturned,
+ "get\(\) returned more than one Article -- it returned 2!",
+ Article.objects.get,
+ pub_date__year=2005,
+ pub_date__month=7,
+ )
+
def test_object_creation(self):
# Create an Article.
a = Article(
diff --git a/tests/modeltests/expressions/tests.py b/tests/modeltests/expressions/tests.py
index 99eb07e370..a351496442 100644
--- a/tests/modeltests/expressions/tests.py
+++ b/tests/modeltests/expressions/tests.py
@@ -158,6 +158,7 @@ class ExpressionsTests(TestCase):
"Max Mustermann",
],
lambda c: six.text_type(c.point_of_contact),
+ ordered=False
)
c = Company.objects.all()[0]
@@ -170,7 +171,8 @@ class ExpressionsTests(TestCase):
"Foobar Ltd.",
"Test GmbH",
],
- lambda c: c.name
+ lambda c: c.name,
+ ordered=False
)
Company.objects.exclude(
@@ -219,3 +221,42 @@ class ExpressionsTests(TestCase):
)
acme.num_employees = F("num_employees") + 16
self.assertRaises(TypeError, acme.save)
+
+ def test_ticket_18375_join_reuse(self):
+ # Test that reverse multijoin F() references and the lookup target
+ # the same join. Pre #18375 the F() join was generated first, and the
+ # lookup couldn't reuse that join.
+ qs = Employee.objects.filter(
+ company_ceo_set__num_chairs=F('company_ceo_set__num_employees'))
+ self.assertEqual(str(qs.query).count('JOIN'), 1)
+
+ def test_ticket_18375_kwarg_ordering(self):
+ # The next query was dict-randomization dependent - if the "gte=1"
+ # was seen first, then the F() will reuse the join generated by the
+ # gte lookup, if F() was seen first, then it generated a join the
+ # other lookups could not reuse.
+ qs = Employee.objects.filter(
+ company_ceo_set__num_chairs=F('company_ceo_set__num_employees'),
+ company_ceo_set__num_chairs__gte=1)
+ self.assertEqual(str(qs.query).count('JOIN'), 1)
+
+ def test_ticket_18375_kwarg_ordering_2(self):
+ # Another similar case for F() than above. Now we have the same join
+ # in two filter kwargs, one in the lhs lookup, one in F. Here pre
+ # #18375 the amount of joins generated was random if dict
+ # randomization was enabled, that is the generated query dependend
+ # on which clause was seen first.
+ qs = Employee.objects.filter(
+ company_ceo_set__num_employees=F('pk'),
+ pk=F('company_ceo_set__num_employees')
+ )
+ self.assertEqual(str(qs.query).count('JOIN'), 1)
+
+ def test_ticket_18375_chained_filters(self):
+ # Test that F() expressions do not reuse joins from previous filter.
+ qs = Employee.objects.filter(
+ company_ceo_set__num_employees=F('pk')
+ ).filter(
+ company_ceo_set__num_employees=F('company_ceo_set__num_employees')
+ )
+ self.assertEqual(str(qs.query).count('JOIN'), 2)
diff --git a/tests/modeltests/field_subclassing/tests.py b/tests/modeltests/field_subclassing/tests.py
index 48755123f2..0ec317dea5 100644
--- a/tests/modeltests/field_subclassing/tests.py
+++ b/tests/modeltests/field_subclassing/tests.py
@@ -77,7 +77,8 @@ class CustomField(TestCase):
"12",
"23",
],
- lambda m: str(m.data)
+ lambda m: str(m.data),
+ ordered=False
)
def test_field_subclassing(self):
diff --git a/tests/modeltests/fixtures/tests.py b/tests/modeltests/fixtures/tests.py
index f9b0ac8a46..415ed6dcf2 100644
--- a/tests/modeltests/fixtures/tests.py
+++ b/tests/modeltests/fixtures/tests.py
@@ -96,8 +96,8 @@ class FixtureLoadingTests(TestCase):
management.call_command('loaddata', 'fixture6.json', verbosity=0, commit=False)
self.assertQuerysetEqual(Tag.objects.all(), [
'<Tag: <Article: Copyright is fine the way it is> tagged "copyright">',
- '<Tag: <Article: Copyright is fine the way it is> tagged "law">'
- ])
+ '<Tag: <Article: Copyright is fine the way it is> tagged "law">',
+ ], ordered=False)
# Load fixture 7, XML file with dynamic ContentType fields. Testing ManyToOne.
management.call_command('loaddata', 'fixture7.xml', verbosity=0, commit=False)
@@ -105,8 +105,8 @@ class FixtureLoadingTests(TestCase):
'<Tag: <Article: Copyright is fine the way it is> tagged "copyright">',
'<Tag: <Article: Copyright is fine the way it is> tagged "legal">',
'<Tag: <Article: Django conquers world!> tagged "django">',
- '<Tag: <Article: Django conquers world!> tagged "world domination">'
- ])
+ '<Tag: <Article: Django conquers world!> tagged "world domination">',
+ ], ordered=False)
# Load fixture 8, JSON file with dynamic Permission fields. Testing ManyToMany.
management.call_command('loaddata', 'fixture8.json', verbosity=0, commit=False)
@@ -114,7 +114,7 @@ class FixtureLoadingTests(TestCase):
'<Visa: Django Reinhardt Can add user, Can change user, Can delete user>',
'<Visa: Stephane Grappelli Can add user>',
'<Visa: Prince >'
- ])
+ ], ordered=False)
# Load fixture 9, XML file with dynamic Permission fields. Testing ManyToMany.
management.call_command('loaddata', 'fixture9.xml', verbosity=0, commit=False)
@@ -122,7 +122,7 @@ class FixtureLoadingTests(TestCase):
'<Visa: Django Reinhardt Can add user, Can change user, Can delete user>',
'<Visa: Stephane Grappelli Can add user, Can delete user>',
'<Visa: Artist formerly known as "Prince" Can change user>'
- ])
+ ], ordered=False)
self.assertQuerysetEqual(Book.objects.all(), [
'<Book: Achieving self-awareness of Python programs>',
@@ -226,9 +226,9 @@ class FixtureLoadingTests(TestCase):
def test_ambiguous_compressed_fixture(self):
# The name "fixture5" is ambigous, so loading it will raise an error
- with six.assertRaisesRegex(self, management.CommandError,
- "Multiple fixtures named 'fixture5'"):
+ with self.assertRaises(management.CommandError) as cm:
management.call_command('loaddata', 'fixture5', verbosity=0, commit=False)
+ self.assertIn("Multiple fixtures named 'fixture5'", cm.exception.args[0])
def test_db_loading(self):
# Load db fixtures 1 and 2. These will load using the 'default' database identifier implicitly
@@ -250,9 +250,9 @@ class FixtureLoadingTests(TestCase):
# is closed at the end of each test.
if connection.vendor == 'mysql':
connection.cursor().execute("SET sql_mode = 'TRADITIONAL'")
- with six.assertRaisesRegex(self, IntegrityError,
- "Could not load fixtures.Article\(pk=1\): .*$"):
+ with self.assertRaises(IntegrityError) as cm:
management.call_command('loaddata', 'invalid.json', verbosity=0, commit=False)
+ self.assertIn("Could not load fixtures.Article(pk=1):", cm.exception.args[0])
def test_loading_using(self):
# Load db fixtures 1 and 2. These will load using the 'default' database identifier explicitly
@@ -280,7 +280,7 @@ class FixtureLoadingTests(TestCase):
self.assertQuerysetEqual(Tag.objects.all(), [
'<Tag: <Article: Time to reform copyright> tagged "copyright">',
'<Tag: <Article: Time to reform copyright> tagged "law">'
- ])
+ ], ordered=False)
# Dump the current contents of the database as a JSON fixture
self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"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": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "law", "tagged_id": 3}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Prince"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_keys=True)
@@ -308,9 +308,9 @@ class FixtureTransactionTests(TransactionTestCase):
# Try to load fixture 2 using format discovery; this will fail
# because there are two fixture2's in the fixtures directory
- with six.assertRaisesRegex(self, management.CommandError,
- "Multiple fixtures named 'fixture2'"):
+ with self.assertRaises(management.CommandError) as cm:
management.call_command('loaddata', 'fixture2', verbosity=0)
+ self.assertIn("Multiple fixtures named 'fixture2'", cm.exception.args[0])
# object list is unaffected
self.assertQuerysetEqual(Article.objects.all(), [
diff --git a/tests/modeltests/generic_relations/tests.py b/tests/modeltests/generic_relations/tests.py
index 14871e4e09..73b0a483a2 100644
--- a/tests/modeltests/generic_relations/tests.py
+++ b/tests/modeltests/generic_relations/tests.py
@@ -169,8 +169,8 @@ class GenericRelationsTests(TestCase):
# Filtering works
self.assertQuerysetEqual(tiger.comparisons.filter(comparative="cooler"), [
"<Comparison: tiger is cooler than cheetah>",
- "<Comparison: tiger is cooler than bear>"
- ])
+ "<Comparison: tiger is cooler than bear>",
+ ], ordered=False)
# Filtering and deleting works
subjective = ["cooler"]
@@ -178,7 +178,7 @@ class GenericRelationsTests(TestCase):
self.assertQuerysetEqual(Comparison.objects.all(), [
"<Comparison: cheetah is faster than tiger>",
"<Comparison: tiger is stronger than cheetah>"
- ])
+ ], ordered=False)
# If we delete cheetah, Comparisons with cheetah as 'first_obj' will be
# deleted since Animal has an explicit GenericRelation to Comparison
diff --git a/tests/modeltests/get_object_or_404/tests.py b/tests/modeltests/get_object_or_404/tests.py
index 280720fd15..3b234c6cd3 100644
--- a/tests/modeltests/get_object_or_404/tests.py
+++ b/tests/modeltests/get_object_or_404/tests.py
@@ -80,3 +80,28 @@ class GetObjectOr404Tests(TestCase):
get_list_or_404(Article.objects.all(), title__icontains="Run"),
[article]
)
+
+ def test_bad_class(self):
+ # Given an argument klass that is not a Model, Manager, or Queryset
+ # raises a helpful ValueError message
+ self.assertRaisesMessage(ValueError,
+ "Object is of type 'str', but must be a Django Model, Manager, "
+ "or QuerySet",
+ get_object_or_404, "Article", title__icontains="Run"
+ )
+
+ class CustomClass(object):
+ pass
+
+ self.assertRaisesMessage(ValueError,
+ "Object is of type 'CustomClass', but must be a Django Model, "
+ "Manager, or QuerySet",
+ get_object_or_404, CustomClass, title__icontains="Run"
+ )
+
+ # Works for lists too
+ self.assertRaisesMessage(ValueError,
+ "Object is of type 'list', but must be a Django Model, Manager, "
+ "or QuerySet",
+ get_list_or_404, [Article], title__icontains="Run"
+ )
diff --git a/tests/modeltests/invalid_models/invalid_models/models.py b/tests/modeltests/invalid_models/invalid_models/models.py
index ccb6396352..3c21e1ddb8 100644
--- a/tests/modeltests/invalid_models/invalid_models/models.py
+++ b/tests/modeltests/invalid_models/invalid_models/models.py
@@ -356,6 +356,13 @@ class HardReferenceModel(models.Model):
m2m_4 = models.ManyToManyField('invalid_models.SwappedModel', related_name='m2m_hardref4')
+class BadIndexTogether1(models.Model):
+ class Meta:
+ index_together = [
+ ["field_that_does_not_exist"],
+ ]
+
+
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute that is a positive integer.
invalid_models.fielderrors: "charfield2": CharFields require a "max_length" attribute that is a positive integer.
invalid_models.fielderrors: "charfield3": CharFields require a "max_length" attribute that is a positive integer.
@@ -470,6 +477,7 @@ invalid_models.hardreferencemodel: 'm2m_3' defines a relation with the model 'in
invalid_models.hardreferencemodel: 'm2m_4' defines a relation with the model 'invalid_models.SwappedModel', which has been swapped out. Update the relation to point at settings.TEST_SWAPPED_MODEL.
invalid_models.badswappablevalue: TEST_SWAPPED_MODEL_BAD_VALUE is not of the form 'app_label.app_name'.
invalid_models.badswappablemodel: Model has been swapped out for 'not_an_app.Target' which has not been installed or is abstract.
+invalid_models.badindextogether1: "index_together" refers to field_that_does_not_exist, a field that doesn't exist.
"""
if not connection.features.interprets_empty_strings_as_nulls:
diff --git a/tests/modeltests/m2m_recursive/tests.py b/tests/modeltests/m2m_recursive/tests.py
index 5742836929..a3f2c670d6 100644
--- a/tests/modeltests/m2m_recursive/tests.py
+++ b/tests/modeltests/m2m_recursive/tests.py
@@ -28,7 +28,8 @@ class RecursiveM2MTests(TestCase):
"Chuck",
"David"
],
- attrgetter("name")
+ attrgetter("name"),
+ ordered=False
)
# Who is friends with Bill?
self.assertQuerysetEqual(
@@ -43,7 +44,8 @@ class RecursiveM2MTests(TestCase):
"Anne",
"David"
],
- attrgetter("name")
+ attrgetter("name"),
+ ordered=False
)
# Who is friends with David?
self.assertQuerysetEqual(
@@ -51,7 +53,8 @@ class RecursiveM2MTests(TestCase):
"Anne",
"Chuck",
],
- attrgetter("name")
+ attrgetter("name"),
+ ordered=False
)
# Bill is already friends with Anne - add Anne again, but in the
# reverse direction
@@ -64,7 +67,8 @@ class RecursiveM2MTests(TestCase):
"Chuck",
"David",
],
- attrgetter("name")
+ attrgetter("name"),
+ ordered=False
)
# Who is friends with Bill?
self.assertQuerysetEqual(
@@ -81,7 +85,8 @@ class RecursiveM2MTests(TestCase):
"Chuck",
"David",
],
- attrgetter("name")
+ attrgetter("name"),
+ ordered=False
)
# Who is friends with Bill?
self.assertQuerysetEqual(
@@ -125,7 +130,8 @@ class RecursiveM2MTests(TestCase):
"Chuck",
"David",
],
- attrgetter("name")
+ attrgetter("name"),
+ ordered=False
)
# Who is stalking Anne?
self.assertQuerysetEqual(
@@ -172,7 +178,8 @@ class RecursiveM2MTests(TestCase):
"Anne",
"Chuck",
],
- attrgetter("name")
+ attrgetter("name"),
+ ordered=False
)
# Bill is already being stalked by Anne - add Anne again, but in the
# reverse direction
@@ -184,7 +191,8 @@ class RecursiveM2MTests(TestCase):
"Chuck",
"David",
],
- attrgetter("name")
+ attrgetter("name"),
+ ordered=False
)
# Who is stalking Anne?
self.assertQuerysetEqual(
@@ -215,7 +223,8 @@ class RecursiveM2MTests(TestCase):
"Chuck",
"David",
],
- attrgetter("name")
+ attrgetter("name"),
+ ordered=False
)
# Who is stalking Anne?
self.assertQuerysetEqual(
diff --git a/tests/modeltests/many_to_one/tests.py b/tests/modeltests/many_to_one/tests.py
index 4fb19dbc69..44ae689dd4 100644
--- a/tests/modeltests/many_to_one/tests.py
+++ b/tests/modeltests/many_to_one/tests.py
@@ -267,7 +267,9 @@ class ManyToOneTests(TestCase):
["<Reporter: John Smith>"])
self.assertQuerysetEqual(
Reporter.objects.filter(article__headline__startswith='T'),
- ["<Reporter: John Smith>", "<Reporter: John Smith>"])
+ ["<Reporter: John Smith>", "<Reporter: John Smith>"],
+ ordered=False
+ )
self.assertQuerysetEqual(
Reporter.objects.filter(article__headline__startswith='T').distinct(),
["<Reporter: John Smith>"])
@@ -285,7 +287,9 @@ class ManyToOneTests(TestCase):
"<Reporter: John Smith>",
"<Reporter: John Smith>",
"<Reporter: John Smith>",
- ])
+ ],
+ ordered=False
+ )
self.assertQuerysetEqual(
Reporter.objects.filter(article__reporter__first_name__startswith='John').distinct(),
["<Reporter: John Smith>"])
diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
index 90b019b611..132da2d318 100644
--- a/tests/modeltests/model_forms/models.py
+++ b/tests/modeltests/model_forms/models.py
@@ -91,15 +91,6 @@ class WriterProfile(models.Model):
def __str__(self):
return "%s is %s" % (self.writer, self.age)
-from django.contrib.localflavor.us.models import PhoneNumberField
-@python_2_unicode_compatible
-class PhoneNumber(models.Model):
- phone = PhoneNumberField()
- description = models.CharField(max_length=20)
-
- def __str__(self):
- return self.phone
-
@python_2_unicode_compatible
class TextFile(models.Model):
description = models.CharField(max_length=20)
diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py
index 947d0cf3c3..9699b155c0 100644
--- a/tests/modeltests/model_forms/tests.py
+++ b/tests/modeltests/model_forms/tests.py
@@ -10,6 +10,7 @@ from django.core.validators import ValidationError
from django.db import connection
from django.db.models.query import EmptyQuerySet
from django.forms.models import model_to_dict
+from django.utils._os import upath
from django.utils.unittest import skipUnless
from django.test import TestCase
from django.utils import six
@@ -17,7 +18,7 @@ from django.utils import six
from .models import (Article, ArticleStatus, BetterWriter, BigInt, Book,
Category, CommaSeparatedInteger, CustomFieldForExclusionModel, DerivedBook,
DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle,
- ImprovedArticleWithParentLink, Inventory, PhoneNumber, Post, Price,
+ ImprovedArticleWithParentLink, Inventory, Post, Price,
Product, TextFile, Writer, WriterProfile, test_images)
if test_images:
@@ -147,10 +148,6 @@ class WriterProfileForm(forms.ModelForm):
class Meta:
model = WriterProfile
-class PhoneNumberForm(forms.ModelForm):
- class Meta:
- model = PhoneNumber
-
class TextFileForm(forms.ModelForm):
class Meta:
model = TextFile
@@ -166,7 +163,7 @@ class ModelFormWithMedia(forms.ModelForm):
'all': ('/some/form/css',)
}
class Meta:
- model = PhoneNumber
+ model = TextFile
class CommaSeparatedIntegerForm(forms.ModelForm):
class Meta:
@@ -561,6 +558,42 @@ class UniqueTest(TestCase):
"slug": "Django 1.0"}, instance=p)
self.assertTrue(form.is_valid())
+class ModelToDictTests(TestCase):
+ """
+ Tests for forms.models.model_to_dict
+ """
+ def test_model_to_dict_many_to_many(self):
+ categories=[
+ Category(name='TestName1', slug='TestName1', url='url1'),
+ Category(name='TestName2', slug='TestName2', url='url2'),
+ Category(name='TestName3', slug='TestName3', url='url3')
+ ]
+ for c in categories:
+ c.save()
+ writer = Writer(name='Test writer')
+ writer.save()
+
+ art = Article(
+ headline='Test article',
+ slug='test-article',
+ pub_date=datetime.date(1988, 1, 4),
+ writer=writer,
+ article='Hello.'
+ )
+ art.save()
+ for c in categories:
+ art.categories.add(c)
+ art.save()
+
+ with self.assertNumQueries(1):
+ d = model_to_dict(art)
+
+ #Ensure all many-to-many categories appear in model_to_dict
+ for c in categories:
+ self.assertIn(c.pk, d['categories'])
+ #Ensure many-to-many relation appears as a list
+ self.assertIsInstance(d['categories'], list)
+
class OldFormForXTests(TestCase):
def test_base_form(self):
self.assertEqual(Category.objects.count(), 0)
@@ -1011,9 +1044,12 @@ class OldFormForXTests(TestCase):
self.assertQuerysetEqual(f.clean([c1.id]), ["Entertainment"])
self.assertQuerysetEqual(f.clean([c2.id]), ["It's a test"])
self.assertQuerysetEqual(f.clean([str(c1.id)]), ["Entertainment"])
- self.assertQuerysetEqual(f.clean([str(c1.id), str(c2.id)]), ["Entertainment", "It's a test"])
- self.assertQuerysetEqual(f.clean([c1.id, str(c2.id)]), ["Entertainment", "It's a test"])
- self.assertQuerysetEqual(f.clean((c1.id, str(c2.id))), ["Entertainment", "It's a test"])
+ self.assertQuerysetEqual(f.clean([str(c1.id), str(c2.id)]), ["Entertainment", "It's a test"],
+ ordered=False)
+ self.assertQuerysetEqual(f.clean([c1.id, str(c2.id)]), ["Entertainment", "It's a test"],
+ ordered=False)
+ self.assertQuerysetEqual(f.clean((c1.id, str(c2.id))), ["Entertainment", "It's a test"],
+ ordered=False)
with self.assertRaises(ValidationError):
f.clean(['100'])
with self.assertRaises(ValidationError):
@@ -1024,7 +1060,10 @@ class OldFormForXTests(TestCase):
# Add a Category object *after* the ModelMultipleChoiceField has already been
# instantiated. This proves clean() checks the database during clean() rather
# than caching it at time of instantiation.
- c6 = Category.objects.create(id=6, name='Sixth', url='6th')
+ # Note, we are using an id of 1006 here since tests that run before
+ # this may create categories with primary keys up to 6. Use
+ # a number that is will not conflict.
+ c6 = Category.objects.create(id=1006, name='Sixth', url='6th')
self.assertEqual(c6.name, 'Sixth')
self.assertQuerysetEqual(f.clean([c6.id]), ["Sixth"])
@@ -1109,12 +1148,6 @@ class OldFormForXTests(TestCase):
</select></p>
<p><label for="id_age">Age:</label> <input type="text" name="age" value="65" id="id_age" /></p>''' % (w_woodward.pk, w_bernstein.pk, bw.pk, w_royko.pk))
- def test_phone_number_field(self):
- f = PhoneNumberForm({'phone': '(312) 555-1212', 'description': 'Assistance'})
- self.assertEqual(f.is_valid(), True)
- self.assertEqual(f.cleaned_data['phone'], '312-555-1212')
- self.assertEqual(f.cleaned_data['description'], 'Assistance')
-
def test_file_field(self):
# Test conditions when files is either not given or empty.
@@ -1243,9 +1276,9 @@ class OldFormForXTests(TestCase):
# it comes to validation. This specifically tests that #6302 is fixed for
# both file fields and image fields.
- with open(os.path.join(os.path.dirname(__file__), "test.png"), 'rb') as fp:
+ with open(os.path.join(os.path.dirname(upath(__file__)), "test.png"), 'rb') as fp:
image_data = fp.read()
- with open(os.path.join(os.path.dirname(__file__), "test2.png"), 'rb') as fp:
+ with open(os.path.join(os.path.dirname(upath(__file__)), "test2.png"), 'rb') as fp:
image_data2 = fp.read()
f = ImageFileForm(
diff --git a/tests/modeltests/pagination/tests.py b/tests/modeltests/pagination/tests.py
deleted file mode 100644
index 12ce0e3ecb..0000000000
--- a/tests/modeltests/pagination/tests.py
+++ /dev/null
@@ -1,134 +0,0 @@
-from __future__ import absolute_import, unicode_literals
-
-from datetime import datetime
-
-from django.core.paginator import Paginator, InvalidPage, EmptyPage
-from django.test import TestCase
-from django.utils import six
-
-from .models import Article
-
-
-class CountContainer(object):
- def count(self):
- return 42
-
-class LenContainer(object):
- def __len__(self):
- return 42
-
-class PaginationTests(TestCase):
- def setUp(self):
- # Prepare a list of objects for pagination.
- for x in range(1, 10):
- a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29))
- a.save()
-
- def test_paginator(self):
- paginator = Paginator(Article.objects.all(), 5)
- self.assertEqual(9, paginator.count)
- self.assertEqual(2, paginator.num_pages)
- self.assertEqual([1, 2], list(paginator.page_range))
-
- def test_first_page(self):
- paginator = Paginator(Article.objects.all(), 5)
- p = paginator.page(1)
- self.assertEqual("<Page 1 of 2>", six.text_type(p))
- self.assertQuerysetEqual(p.object_list, [
- "<Article: Article 1>",
- "<Article: Article 2>",
- "<Article: Article 3>",
- "<Article: Article 4>",
- "<Article: Article 5>"
- ]
- )
- self.assertTrue(p.has_next())
- self.assertFalse(p.has_previous())
- self.assertTrue(p.has_other_pages())
- self.assertEqual(2, p.next_page_number())
- self.assertRaises(InvalidPage, p.previous_page_number)
- self.assertEqual(1, p.start_index())
- self.assertEqual(5, p.end_index())
-
- def test_last_page(self):
- paginator = Paginator(Article.objects.all(), 5)
- p = paginator.page(2)
- self.assertEqual("<Page 2 of 2>", six.text_type(p))
- self.assertQuerysetEqual(p.object_list, [
- "<Article: Article 6>",
- "<Article: Article 7>",
- "<Article: Article 8>",
- "<Article: Article 9>"
- ]
- )
- self.assertFalse(p.has_next())
- self.assertTrue(p.has_previous())
- self.assertTrue(p.has_other_pages())
- self.assertRaises(InvalidPage, p.next_page_number)
- self.assertEqual(1, p.previous_page_number())
- self.assertEqual(6, p.start_index())
- self.assertEqual(9, p.end_index())
-
- def test_empty_page(self):
- paginator = Paginator(Article.objects.all(), 5)
- self.assertRaises(EmptyPage, paginator.page, 0)
- self.assertRaises(EmptyPage, paginator.page, 3)
-
- # Empty paginators with allow_empty_first_page=True.
- paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=True)
- self.assertEqual(0, paginator.count)
- self.assertEqual(1, paginator.num_pages)
- self.assertEqual([1], list(paginator.page_range))
-
- # Empty paginators with allow_empty_first_page=False.
- paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=False)
- self.assertEqual(0, paginator.count)
- self.assertEqual(0, paginator.num_pages)
- self.assertEqual([], list(paginator.page_range))
-
- def test_invalid_page(self):
- paginator = Paginator(Article.objects.all(), 5)
- self.assertRaises(InvalidPage, paginator.page, 7)
-
- def test_orphans(self):
- # Add a few more records to test out the orphans feature.
- for x in range(10, 13):
- Article(headline="Article %s" % x, pub_date=datetime(2006, 10, 6)).save()
-
- # With orphans set to 3 and 10 items per page, we should get all 12 items on a single page.
- paginator = Paginator(Article.objects.all(), 10, orphans=3)
- self.assertEqual(1, paginator.num_pages)
-
- # With orphans only set to 1, we should get two pages.
- paginator = Paginator(Article.objects.all(), 10, orphans=1)
- self.assertEqual(2, paginator.num_pages)
-
- def test_paginate_list(self):
- # Paginators work with regular lists/tuples, too -- not just with QuerySets.
- paginator = Paginator([1, 2, 3, 4, 5, 6, 7, 8, 9], 3)
- self.assertEqual(9, paginator.count)
- self.assertEqual(3, paginator.num_pages)
- self.assertEqual([1, 2, 3], list(paginator.page_range))
- p = paginator.page(2)
- self.assertEqual("<Page 2 of 3>", six.text_type(p))
- self.assertEqual([4, 5, 6], p.object_list)
- self.assertTrue(p.has_next())
- self.assertTrue(p.has_previous())
- self.assertTrue(p.has_other_pages())
- self.assertEqual(3, p.next_page_number())
- self.assertEqual(1, p.previous_page_number())
- self.assertEqual(4, p.start_index())
- self.assertEqual(6, p.end_index())
-
- def test_paginate_misc_classes(self):
- # Paginator can be passed other objects with a count() method.
- paginator = Paginator(CountContainer(), 10)
- self.assertEqual(42, paginator.count)
- self.assertEqual(5, paginator.num_pages)
- self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range))
-
- # Paginator can be passed other objects that implement __len__.
- paginator = Paginator(LenContainer(), 10)
- self.assertEqual(42, paginator.count)
- self.assertEqual(5, paginator.num_pages)
- self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range))
diff --git a/tests/modeltests/prefetch_related/models.py b/tests/modeltests/prefetch_related/models.py
index 85488f0879..e58997d200 100644
--- a/tests/modeltests/prefetch_related/models.py
+++ b/tests/modeltests/prefetch_related/models.py
@@ -125,6 +125,10 @@ class TaggedItem(models.Model):
related_name='taggeditem_set3')
created_by_fkey = models.PositiveIntegerField(null=True)
created_by = generic.GenericForeignKey('created_by_ct', 'created_by_fkey',)
+ favorite_ct = models.ForeignKey(ContentType, null=True,
+ related_name='taggeditem_set4')
+ favorite_fkey = models.CharField(max_length=64, null=True)
+ favorite = generic.GenericForeignKey('favorite_ct', 'favorite_fkey')
def __str__(self):
return self.tag
@@ -132,7 +136,11 @@ class TaggedItem(models.Model):
class Bookmark(models.Model):
url = models.URLField()
- tags = generic.GenericRelation(TaggedItem)
+ tags = generic.GenericRelation(TaggedItem, related_name='bookmarks')
+ favorite_tags = generic.GenericRelation(TaggedItem,
+ content_type_field='favorite_ct',
+ object_id_field='favorite_fkey',
+ related_name='favorite_bookmarks')
class Comment(models.Model):
diff --git a/tests/modeltests/prefetch_related/tests.py b/tests/modeltests/prefetch_related/tests.py
index 614a5fc1d6..e81560f01f 100644
--- a/tests/modeltests/prefetch_related/tests.py
+++ b/tests/modeltests/prefetch_related/tests.py
@@ -319,6 +319,16 @@ class GenericRelationTests(TestCase):
for t in b.tags.all()]
self.assertEqual(sorted(tags), ["django", "python"])
+ def test_charfield_GFK(self):
+ b = Bookmark.objects.create(url='http://www.djangoproject.com/')
+ t1 = TaggedItem.objects.create(content_object=b, tag='django')
+ t2 = TaggedItem.objects.create(content_object=b, favorite=b, tag='python')
+
+ with self.assertNumQueries(3):
+ bookmark = Bookmark.objects.filter(pk=b.pk).prefetch_related('tags', 'favorite_tags')[0]
+ self.assertEqual(sorted([i.tag for i in bookmark.tags.all()]), ["django", "python"])
+ self.assertEqual([i.tag for i in bookmark.favorite_tags.all()], ["python"])
+
class MultiTableInheritanceTest(TestCase):
diff --git a/tests/modeltests/proxy_model_inheritance/tests.py b/tests/modeltests/proxy_model_inheritance/tests.py
index 39fee7ee6d..239bc67809 100644
--- a/tests/modeltests/proxy_model_inheritance/tests.py
+++ b/tests/modeltests/proxy_model_inheritance/tests.py
@@ -8,6 +8,7 @@ from django.core.management import call_command
from django.db.models.loading import cache, load_app
from django.test import TestCase, TransactionTestCase
from django.test.utils import override_settings
+from django.utils._os import upath
from .models import (ConcreteModel, ConcreteModelSubclass,
ConcreteModelSubclassProxy)
@@ -23,7 +24,7 @@ class ProxyModelInheritanceTests(TransactionTestCase):
def setUp(self):
self.old_sys_path = sys.path[:]
- sys.path.append(os.path.dirname(os.path.abspath(__file__)))
+ sys.path.append(os.path.dirname(os.path.abspath(upath(__file__))))
for app in settings.INSTALLED_APPS:
load_app(app)
diff --git a/tests/modeltests/raw_query/models.py b/tests/modeltests/raw_query/models.py
index 823fc76eee..e7e221dc6e 100644
--- a/tests/modeltests/raw_query/models.py
+++ b/tests/modeltests/raw_query/models.py
@@ -13,7 +13,7 @@ class Author(models.Model):
# treated differently than fields.
for k in kwargs:
assert k in [f.attname for f in self._meta.fields], \
- "Author.__init__ got an unexpected paramater: %s" % k
+ "Author.__init__ got an unexpected parameter: %s" % k
class Book(models.Model):
title = models.CharField(max_length=255)
diff --git a/tests/modeltests/str/models.py b/tests/modeltests/str/models.py
index 488012e861..1c158ea008 100644
--- a/tests/modeltests/str/models.py
+++ b/tests/modeltests/str/models.py
@@ -28,6 +28,14 @@ class Article(models.Model):
return self.headline
@python_2_unicode_compatible
+class BrokenArticle(models.Model):
+ headline = models.CharField(max_length=100)
+ pub_date = models.DateTimeField()
+
+ def __unicode__(self): # instead of __str__
+ return self.headline
+
+@python_2_unicode_compatible
class InternationalArticle(models.Model):
headline = models.CharField(max_length=100)
pub_date = models.DateTimeField()
diff --git a/tests/modeltests/str/tests.py b/tests/modeltests/str/tests.py
index 2c11ac8c78..31869583aa 100644
--- a/tests/modeltests/str/tests.py
+++ b/tests/modeltests/str/tests.py
@@ -7,7 +7,7 @@ from django.test import TestCase
from django.utils import six
from django.utils.unittest import skipIf
-from .models import Article, InternationalArticle
+from .models import Article, BrokenArticle, InternationalArticle
class SimpleTests(TestCase):
@@ -21,6 +21,16 @@ class SimpleTests(TestCase):
self.assertEqual(str(a), str('Area man programs in Python'))
self.assertEqual(repr(a), str('<Article: Area man programs in Python>'))
+ @skipIf(six.PY3, "tests Model's default __str__ method under Python 2")
+ def test_broken(self):
+ # Regression test for #19362.
+ a = BrokenArticle.objects.create(
+ headline='Girl wins €12.500 in lottery',
+ pub_date=datetime.datetime(2005, 7, 28)
+ )
+ self.assertRaisesRegexp(RuntimeError, "Did you apply "
+ "@python_2_unicode_compatible without defining __str__\?", str, a)
+
def test_international(self):
a = InternationalArticle.objects.create(
headline='Girl wins €12.500 in lottery',
diff --git a/tests/regressiontests/admin_changelist/admin.py b/tests/regressiontests/admin_changelist/admin.py
index 9ecfbc6e12..5751d04bce 100644
--- a/tests/regressiontests/admin_changelist/admin.py
+++ b/tests/regressiontests/admin_changelist/admin.py
@@ -32,6 +32,7 @@ class ParentAdmin(admin.ModelAdmin):
class ChildAdmin(admin.ModelAdmin):
list_display = ['name', 'parent']
list_per_page = 10
+ list_filter = ['parent', 'age']
def queryset(self, request):
return super(ChildAdmin, self).queryset(request).select_related("parent__name")
@@ -90,3 +91,14 @@ class SwallowAdmin(admin.ModelAdmin):
list_display = ('origin', 'load', 'speed')
site.register(Swallow, SwallowAdmin)
+
+class DynamicListFilterChildAdmin(admin.ModelAdmin):
+ list_filter = ('parent', 'name', 'age')
+
+ def get_list_filter(self, request):
+ my_list_filter = super(DynamicListFilterChildAdmin, self).get_list_filter(request)
+ if request.user.username == 'noparents':
+ my_list_filter = list(my_list_filter)
+ my_list_filter.remove('parent')
+ return my_list_filter
+
diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py
index 2b1c1a9bcf..e8d4cbff16 100644
--- a/tests/regressiontests/admin_changelist/tests.py
+++ b/tests/regressiontests/admin_changelist/tests.py
@@ -17,7 +17,7 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin,
GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin,
DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin,
FilteredChildAdmin, CustomPaginator, site as custom_site,
- SwallowAdmin)
+ SwallowAdmin, DynamicListFilterChildAdmin)
from .models import (Event, Child, Parent, Genre, Band, Musician, Group,
Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow,
UnorderedObject, OrderedObject)
@@ -541,3 +541,26 @@ class ChangeListTests(TestCase):
check_results_order()
OrderedObjectAdmin.ordering = ['id', 'bool']
check_results_order(ascending=True)
+
+ def test_dynamic_list_filter(self):
+ """
+ Regression tests for ticket #17646: dynamic list_filter support.
+ """
+ parent = Parent.objects.create(name='parent')
+ for i in range(10):
+ Child.objects.create(name='child %s' % i, parent=parent)
+
+ user_noparents = self._create_superuser('noparents')
+ user_parents = self._create_superuser('parents')
+
+ # Test with user 'noparents'
+ m = DynamicListFilterChildAdmin(Child, admin.site)
+ request = self._mocked_authenticated_request('/child/', user_noparents)
+ response = m.changelist_view(request)
+ self.assertEqual(response.context_data['cl'].list_filter, ['name', 'age'])
+
+ # Test with user 'parents'
+ m = DynamicListFilterChildAdmin(Child, admin.site)
+ request = self._mocked_authenticated_request('/child/', user_parents)
+ response = m.changelist_view(request)
+ self.assertEqual(response.context_data['cl'].list_filter, ('parent', 'name', 'age'))
diff --git a/tests/regressiontests/admin_filters/tests.py b/tests/regressiontests/admin_filters/tests.py
index 6a36f62cea..4a3e6135c3 100644
--- a/tests/regressiontests/admin_filters/tests.py
+++ b/tests/regressiontests/admin_filters/tests.py
@@ -78,6 +78,21 @@ class DecadeListFilterParameterEndsWith__Isnull(DecadeListFilter):
parameter_name = 'decade__isnull' # Ends with '__isnull"
+class DepartmentListFilterLookupWithNonStringValue(SimpleListFilter):
+ title = 'department'
+ parameter_name = 'department'
+
+ def lookups(self, request, model_admin):
+ return set([
+ (employee.department.id, # Intentionally not a string (Refs #19318)
+ employee.department.code)
+ for employee in model_admin.queryset(request).all()
+ ])
+
+ def queryset(self, request, queryset):
+ if self.value():
+ return queryset.filter(department__id=self.value())
+
class CustomUserAdmin(UserAdmin):
list_filter = ('books_authored', 'books_contributed')
@@ -118,6 +133,10 @@ class EmployeeAdmin(ModelAdmin):
list_filter = ['department']
+class DepartmentFilterEmployeeAdmin(EmployeeAdmin):
+ list_filter = [DepartmentListFilterLookupWithNonStringValue, ]
+
+
class ListFiltersTests(TestCase):
def setUp(self):
@@ -140,6 +159,14 @@ class ListFiltersTests(TestCase):
self.gipsy_book.contributors = [self.bob, self.lisa]
self.gipsy_book.save()
+ # Departments
+ self.dev = Department.objects.create(code='DEV', description='Development')
+ self.design = Department.objects.create(code='DSN', description='Design')
+
+ # Employees
+ self.john = Employee.objects.create(name='John Blue', department=self.dev)
+ self.jack = Employee.objects.create(name='Jack Red', department=self.design)
+
def get_changelist(self, request, model, modeladmin):
return ChangeList(request, model, modeladmin.list_display, modeladmin.list_display_links,
modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
@@ -638,6 +665,28 @@ class ListFiltersTests(TestCase):
self.assertEqual(choices[2]['selected'], True)
self.assertEqual(choices[2]['query_string'], '?decade__isnull=the+90s')
+ def test_lookup_with_non_string_value(self):
+ """
+ Ensure choices are set the selected class when using non-string values
+ for lookups in SimpleListFilters.
+ Refs #19318
+ """
+
+ modeladmin = DepartmentFilterEmployeeAdmin(Employee, site)
+ request = self.request_factory.get('/', {'department': self.john.pk})
+ changelist = self.get_changelist(request, Employee, modeladmin)
+
+ queryset = changelist.get_query_set(request)
+
+ self.assertEqual(list(queryset), [self.john])
+
+ filterspec = changelist.get_filters(request)[0][-1]
+ self.assertEqual(force_text(filterspec.title), 'department')
+ choices = list(filterspec.choices(changelist))
+ self.assertEqual(choices[2]['display'], 'DEV')
+ self.assertEqual(choices[2]['selected'], True)
+ self.assertEqual(choices[2]['query_string'], '?department=%s' % self.john.pk)
+
def test_fk_with_to_field(self):
"""
Ensure that a filter on a FK respects the FK's to_field attribute.
@@ -645,17 +694,12 @@ class ListFiltersTests(TestCase):
"""
modeladmin = EmployeeAdmin(Employee, site)
- dev = Department.objects.create(code='DEV', description='Development')
- design = Department.objects.create(code='DSN', description='Design')
- john = Employee.objects.create(name='John Blue', department=dev)
- jack = Employee.objects.create(name='Jack Red', department=design)
-
request = self.request_factory.get('/', {})
changelist = self.get_changelist(request, Employee, modeladmin)
# Make sure the correct queryset is returned
queryset = changelist.get_query_set(request)
- self.assertEqual(list(queryset), [jack, john])
+ self.assertEqual(list(queryset), [self.jack, self.john])
filterspec = changelist.get_filters(request)[0][-1]
self.assertEqual(force_text(filterspec.title), 'department')
@@ -680,7 +724,7 @@ class ListFiltersTests(TestCase):
# Make sure the correct queryset is returned
queryset = changelist.get_query_set(request)
- self.assertEqual(list(queryset), [john])
+ self.assertEqual(list(queryset), [self.john])
filterspec = changelist.get_filters(request)[0][-1]
self.assertEqual(force_text(filterspec.title), 'department')
diff --git a/tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-19397-binary-file.ico b/tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-19397-binary-file.ico
new file mode 100644
index 0000000000..1db49645b5
--- /dev/null
+++ b/tests/regressiontests/admin_scripts/custom_templates/project_template/ticket-19397-binary-file.ico
Binary files differ
diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py
index 3bb8bb0b50..d0ca9d26df 100644
--- a/tests/regressiontests/admin_scripts/tests.py
+++ b/tests/regressiontests/admin_scripts/tests.py
@@ -19,13 +19,15 @@ from django.conf import settings
from django.db import connection
from django.test.simple import DjangoTestSuiteRunner
from django.utils import unittest
+from django.utils.encoding import force_str, force_text
+from django.utils._os import upath
from django.test import LiveServerTestCase
-test_dir = os.path.dirname(os.path.dirname(__file__))
+test_dir = os.path.dirname(os.path.dirname(upath(__file__)))
class AdminScriptTestCase(unittest.TestCase):
def write_settings(self, filename, apps=None, is_dir=False, sdict=None):
- test_dir = os.path.dirname(os.path.dirname(__file__))
+ test_dir = os.path.dirname(os.path.dirname(upath(__file__)))
if is_dir:
settings_dir = os.path.join(test_dir, filename)
os.mkdir(settings_dir)
@@ -94,6 +96,7 @@ class AdminScriptTestCase(unittest.TestCase):
return paths
def run_test(self, script, args, settings_file=None, apps=None):
+ test_dir = os.path.dirname(os.path.dirname(__file__))
project_dir = os.path.dirname(test_dir)
base_dir = os.path.dirname(project_dir)
ext_backend_base_dirs = self._ext_backend_paths()
@@ -134,11 +137,17 @@ class AdminScriptTestCase(unittest.TestCase):
return out, err
def run_django_admin(self, args, settings_file=None):
- bin_dir = os.path.abspath(os.path.dirname(bin.__file__))
+ bin_dir = os.path.abspath(os.path.dirname(upath(bin.__file__)))
return self.run_test(os.path.join(bin_dir, 'django-admin.py'), args, settings_file)
def run_manage(self, args, settings_file=None):
- conf_dir = os.path.dirname(conf.__file__)
+ def safe_remove(path):
+ try:
+ os.remove(path)
+ except OSError:
+ pass
+
+ conf_dir = os.path.dirname(upath(conf.__file__))
template_manage_py = os.path.join(conf_dir, 'project_template', 'manage.py')
test_manage_py = os.path.join(test_dir, 'manage.py')
@@ -150,13 +159,9 @@ class AdminScriptTestCase(unittest.TestCase):
"{{ project_name }}", "regressiontests")
with open(test_manage_py, 'w') as fp:
fp.write(manage_py_contents)
+ self.addCleanup(safe_remove, test_manage_py)
- stdout, stderr = self.run_test('./manage.py', args, settings_file)
-
- # Cleanup - remove the generated manage.py script
- os.remove(test_manage_py)
-
- return stdout, stderr
+ return self.run_test('./manage.py', args, settings_file)
def assertNoOutput(self, stream):
"Utility assertion: assert that the given stream is empty"
@@ -164,10 +169,12 @@ class AdminScriptTestCase(unittest.TestCase):
def assertOutput(self, stream, msg):
"Utility assertion: assert that the given message exists in the output"
+ stream = force_text(stream)
self.assertTrue(msg in stream, "'%s' does not match actual output text '%s'" % (msg, stream))
def assertNotInOutput(self, stream, msg):
"Utility assertion: assert that the given message doesn't exist in the output"
+ stream = force_text(stream)
self.assertFalse(msg in stream, "'%s' matches actual output text '%s'" % (msg, stream))
##########################################################################
@@ -1410,9 +1417,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
"Make sure the startproject management command creates a project"
args = ['startproject', 'testproject']
testproject_dir = os.path.join(test_dir, 'testproject')
+ self.addCleanup(shutil.rmtree, testproject_dir, True)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.isdir(testproject_dir))
@@ -1423,16 +1430,11 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
def test_invalid_project_name(self):
"Make sure the startproject management command validates a project name"
-
- def cleanup(p):
- if os.path.exists(p):
- shutil.rmtree(p)
-
args = ['startproject', '7testproject']
testproject_dir = os.path.join(test_dir, '7testproject')
+ self.addCleanup(shutil.rmtree, testproject_dir, True)
out, err = self.run_django_admin(args)
- self.addCleanup(cleanup, testproject_dir)
self.assertOutput(err, "Error: '7testproject' is not a valid project name. Please make sure the name begins with a letter or underscore.")
self.assertFalse(os.path.exists(testproject_dir))
@@ -1441,9 +1443,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
args = ['startproject', 'testproject', 'othertestproject']
testproject_dir = os.path.join(test_dir, 'othertestproject')
os.mkdir(testproject_dir)
+ self.addCleanup(shutil.rmtree, testproject_dir)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'manage.py')))
@@ -1457,9 +1459,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template')
args = ['startproject', '--template', template_path, 'customtestproject']
testproject_dir = os.path.join(test_dir, 'customtestproject')
+ self.addCleanup(shutil.rmtree, testproject_dir, True)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.isdir(testproject_dir))
self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'additional_dir')))
@@ -1469,9 +1471,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template' + os.sep)
args = ['startproject', '--template', template_path, 'customtestproject']
testproject_dir = os.path.join(test_dir, 'customtestproject')
+ self.addCleanup(shutil.rmtree, testproject_dir, True)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.isdir(testproject_dir))
self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'additional_dir')))
@@ -1481,9 +1483,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template.tgz')
args = ['startproject', '--template', template_path, 'tarballtestproject']
testproject_dir = os.path.join(test_dir, 'tarballtestproject')
+ self.addCleanup(shutil.rmtree, testproject_dir, True)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.isdir(testproject_dir))
self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'run.py')))
@@ -1494,9 +1496,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
args = ['startproject', '--template', template_path, 'tarballtestproject', 'altlocation']
testproject_dir = os.path.join(test_dir, 'altlocation')
os.mkdir(testproject_dir)
+ self.addCleanup(shutil.rmtree, testproject_dir)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.isdir(testproject_dir))
self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'run.py')))
@@ -1507,9 +1509,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
args = ['startproject', '--template', template_url, 'urltestproject']
testproject_dir = os.path.join(test_dir, 'urltestproject')
+ self.addCleanup(shutil.rmtree, testproject_dir, True)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.isdir(testproject_dir))
self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'run.py')))
@@ -1520,9 +1522,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
args = ['startproject', '--template', template_url, 'urltestproject']
testproject_dir = os.path.join(test_dir, 'urltestproject')
+ self.addCleanup(shutil.rmtree, testproject_dir, True)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.isdir(testproject_dir))
self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'run.py')))
@@ -1532,9 +1534,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template')
args = ['startproject', '--template', template_path, 'customtestproject', '-e', 'txt', '-n', 'Procfile']
testproject_dir = os.path.join(test_dir, 'customtestproject')
+ self.addCleanup(shutil.rmtree, testproject_dir, True)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.isdir(testproject_dir))
self.assertTrue(os.path.exists(os.path.join(testproject_dir, 'additional_dir')))
@@ -1551,12 +1553,12 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
args = ['startproject', '--template', template_path, 'another_project', 'project_dir']
testproject_dir = os.path.join(test_dir, 'project_dir')
os.mkdir(testproject_dir)
- out, err = self.run_django_admin(args)
self.addCleanup(shutil.rmtree, testproject_dir)
+ out, err = self.run_django_admin(args)
self.assertNoOutput(err)
test_manage_py = os.path.join(testproject_dir, 'manage.py')
with open(test_manage_py, 'r') as fp:
- content = fp.read()
+ content = force_text(fp.read())
self.assertIn("project_name = 'another_project'", content)
self.assertIn("project_directory = '%s'" % testproject_dir, content)
@@ -1564,19 +1566,18 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
"Make sure template context variables are not html escaped"
# We're using a custom command so we need the alternate settings
self.write_settings('alternate_settings.py')
+ self.addCleanup(self.remove_settings, 'alternate_settings.py')
template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template')
args = ['custom_startproject', '--template', template_path, 'another_project', 'project_dir', '--extra', '<&>', '--settings=alternate_settings']
testproject_dir = os.path.join(test_dir, 'project_dir')
os.mkdir(testproject_dir)
- out, err = self.run_manage(args)
self.addCleanup(shutil.rmtree, testproject_dir)
+ out, err = self.run_manage(args)
self.assertNoOutput(err)
test_manage_py = os.path.join(testproject_dir, 'additional_dir', 'extra.py')
with open(test_manage_py, 'r') as fp:
content = fp.read()
self.assertIn("<&>", content)
- # tidy up alternate settings
- self.remove_settings('alternate_settings.py')
def test_custom_project_destination_missing(self):
"""
@@ -1596,9 +1597,9 @@ class StartProject(LiveServerTestCase, AdminScriptTestCase):
template_path = os.path.join(test_dir, 'admin_scripts', 'custom_templates', 'project_template')
args = ['startproject', '--template', template_path, '--extension=txt', 'customtestproject']
testproject_dir = os.path.join(test_dir, 'customtestproject')
+ self.addCleanup(shutil.rmtree, testproject_dir, True)
out, err = self.run_django_admin(args)
- self.addCleanup(shutil.rmtree, testproject_dir)
self.assertNoOutput(err)
self.assertTrue(os.path.isdir(testproject_dir))
path = os.path.join(testproject_dir, 'ticket-18091-non-ascii-template.txt')
@@ -1612,8 +1613,8 @@ class DiffSettings(AdminScriptTestCase):
def test_basic(self):
"Runs without error and emits settings diff."
self.write_settings('settings_to_diff.py', sdict={'FOO': '"bar"'})
+ self.addCleanup(self.remove_settings, 'settings_to_diff.py')
args = ['diffsettings', '--settings=settings_to_diff']
out, err = self.run_manage(args)
- self.remove_settings('settings_to_diff.py')
self.assertNoOutput(err)
self.assertOutput(out, "FOO = 'bar' ###")
diff --git a/tests/regressiontests/admin_scripts/urls.py b/tests/regressiontests/admin_scripts/urls.py
index 692638ceca..a45dc3e9a6 100644
--- a/tests/regressiontests/admin_scripts/urls.py
+++ b/tests/regressiontests/admin_scripts/urls.py
@@ -1,7 +1,8 @@
import os
from django.conf.urls import patterns
+from django.utils._os import upath
-here = os.path.dirname(__file__)
+here = os.path.dirname(upath(__file__))
urlpatterns = patterns('',
(r'^custom_templates/(?P<path>.*)$', 'django.views.static.serve', {
diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py
index ef8a91d1db..e9e122a9f0 100644
--- a/tests/regressiontests/admin_util/tests.py
+++ b/tests/regressiontests/admin_util/tests.py
@@ -274,6 +274,10 @@ class UtilTests(unittest.TestCase):
six.text_type(log_entry).startswith('Deleted ')
)
+ # Make sure custom action_flags works
+ log_entry.action_flag = 4
+ self.assertEqual(six.text_type(log_entry), 'LogEntry Object')
+
def test_safestring_in_field_label(self):
# safestring should not be escaped
class MyForm(forms.Form):
diff --git a/tests/regressiontests/admin_views/admin.py b/tests/regressiontests/admin_views/admin.py
index a5476e9eb7..1e4e9b5c75 100644
--- a/tests/regressiontests/admin_views/admin.py
+++ b/tests/regressiontests/admin_views/admin.py
@@ -27,7 +27,7 @@ from .models import (Article, Chapter, Account, Media, Child, Parent, Picture,
Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug,
AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod,
AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated,
- RelatedPrepopulated, UndeletableObject, Simple)
+ RelatedPrepopulated, UndeletableObject, UserMessenger, Simple, Choice)
def callable_year(dt_value):
@@ -346,7 +346,10 @@ class LinkInline(admin.TabularInline):
model = Link
extra = 1
- readonly_fields = ("posted",)
+ readonly_fields = ("posted", "multiline")
+
+ def multiline(self, instance):
+ return "InlineMultiline\ntest\nstring"
class SubPostInline(admin.TabularInline):
@@ -388,7 +391,10 @@ class PrePopulatedPostAdmin(admin.ModelAdmin):
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'public']
- readonly_fields = ('posted', 'awesomeness_level', 'coolness', 'value', lambda obj: "foo")
+ readonly_fields = (
+ 'posted', 'awesomeness_level', 'coolness', 'value', 'multiline',
+ lambda obj: "foo"
+ )
inlines = [
LinkInline
@@ -402,6 +408,10 @@ class PostAdmin(admin.ModelAdmin):
def value(self, instance):
return 1000
+
+ def multiline(self, instance):
+ return "Multiline\ntest\nstring"
+
value.short_description = 'Value in $US'
@@ -585,6 +595,34 @@ def callable_on_unknown(obj):
class AttributeErrorRaisingAdmin(admin.ModelAdmin):
list_display = [callable_on_unknown, ]
+class MessageTestingAdmin(admin.ModelAdmin):
+ actions = ["message_debug", "message_info", "message_success",
+ "message_warning", "message_error", "message_extra_tags"]
+
+ def message_debug(self, request, selected):
+ self.message_user(request, "Test debug", level="debug")
+
+ def message_info(self, request, selected):
+ self.message_user(request, "Test info", level="info")
+
+ def message_success(self, request, selected):
+ self.message_user(request, "Test success", level="success")
+
+ def message_warning(self, request, selected):
+ self.message_user(request, "Test warning", level="warning")
+
+ def message_error(self, request, selected):
+ self.message_user(request, "Test error", level="error")
+
+ def message_extra_tags(self, request, selected):
+ self.message_user(request, "Test tags", extra_tags="extra_tag")
+
+
+class ChoiceList(admin.ModelAdmin):
+ list_display = ['choice']
+ readonly_fields = ['choice']
+ fields = ['choice']
+
site = admin.AdminSite(name="admin")
site.register(Article, ArticleAdmin)
@@ -660,6 +698,8 @@ site.register(AdminOrderedAdminMethod, AdminOrderedAdminMethodAdmin)
site.register(AdminOrderedCallable, AdminOrderedCallableAdmin)
site.register(Color2, CustomTemplateFilterColorAdmin)
site.register(Simple, AttributeErrorRaisingAdmin)
+site.register(UserMessenger, MessageTestingAdmin)
+site.register(Choice, ChoiceList)
# Register core models we need in our tests
from django.contrib.auth.models import User, Group
diff --git a/tests/regressiontests/admin_views/customadmin.py b/tests/regressiontests/admin_views/customadmin.py
index 031fb50f0f..d69d690af0 100644
--- a/tests/regressiontests/admin_views/customadmin.py
+++ b/tests/regressiontests/admin_views/customadmin.py
@@ -16,7 +16,7 @@ class Admin2(admin.AdminSite):
login_form = forms.CustomAdminAuthenticationForm
login_template = 'custom_admin/login.html'
logout_template = 'custom_admin/logout.html'
- index_template = 'custom_admin/index.html'
+ index_template = ['custom_admin/index.html'] # a list, to test fix for #18697
password_change_template = 'custom_admin/password_change_form.html'
password_change_done_template = 'custom_admin/password_change_done.html'
@@ -40,6 +40,10 @@ class UserLimitedAdmin(UserAdmin):
return qs.filter(is_superuser=False)
+class CustomPwdTemplateUserAdmin(UserAdmin):
+ change_user_password_template = ['admin/auth/user/change_password.html'] # a list, to test fix for #18697
+
+
site = Admin2(name="admin2")
site.register(models.Article, base_admin.ArticleAdmin)
@@ -50,3 +54,6 @@ site.register(models.ChapterXtra1, base_admin.ChapterXtra1Admin)
site.register(User, UserLimitedAdmin)
site.register(models.UndeletableObject, base_admin.UndeletableObjectAdmin)
site.register(models.Simple, base_admin.AttributeErrorRaisingAdmin)
+
+simple_site = Admin2(name='admin4')
+simple_site.register(User, CustomPwdTemplateUserAdmin)
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
index 2b143004d9..c641cfc0e6 100644
--- a/tests/regressiontests/admin_views/models.py
+++ b/tests/regressiontests/admin_views/models.py
@@ -651,8 +651,16 @@ class UndeletableObject(models.Model):
"""
name = models.CharField(max_length=255)
+class UserMessenger(models.Model):
+ """
+ Dummy class for testing message_user functions on ModelAdmin
+ """
class Simple(models.Model):
"""
Simple model with nothing on it for use in testing
"""
+
+class Choice(models.Model):
+ choice = models.IntegerField(blank=True, null=True,
+ choices=((1, 'Yes'), (0, 'No'), (None, 'No opinion')))
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
index 72dc6a3f97..669695ea9b 100644
--- a/tests/regressiontests/admin_views/tests.py
+++ b/tests/regressiontests/admin_views/tests.py
@@ -33,6 +33,7 @@ from django.utils.cache import get_max_age
from django.utils.encoding import iri_to_uri, force_bytes
from django.utils.html import escape
from django.utils.http import urlencode
+from django.utils._os import upath
from django.utils import six
from django.test.utils import override_settings
@@ -46,11 +47,11 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount,
OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField,
AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable,
Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject,
- Simple, UndeletableObject)
+ Simple, UndeletableObject, Choice)
ERROR_MESSAGE = "Please enter the correct username and password \
-for a staff account. Note that both fields are case-sensitive."
+for a staff account. Note that both fields may be case-sensitive."
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
@@ -633,7 +634,7 @@ class AdminViewFormUrlTest(TestCase):
Refs #17515.
"""
template_dirs = settings.TEMPLATE_DIRS + (
- os.path.join(os.path.dirname(__file__), 'templates'),)
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),)
with self.settings(TEMPLATE_DIRS=template_dirs):
response = self.client.get("/test_admin/admin/admin_views/color2/")
self.assertTrue('custom_filter_template.html' in [t.name for t in response.templates])
@@ -770,7 +771,10 @@ class CustomModelAdminTest(AdminViewBasicTest):
self.assertContains(response, 'Hello from a custom logout template')
def testCustomAdminSiteIndexViewAndTemplate(self):
- response = self.client.get('/test_admin/admin2/')
+ try:
+ response = self.client.get('/test_admin/admin2/')
+ except TypeError:
+ self.fail('AdminSite.index_template should accept a list of template paths')
self.assertIsInstance(response, TemplateResponse)
self.assertTemplateUsed(response, 'custom_admin/index.html')
self.assertContains(response, 'Hello from a custom index template *bar*')
@@ -792,6 +796,15 @@ class CustomModelAdminTest(AdminViewBasicTest):
response = self.client.get('/test_admin/%s/my_view/' % self.urlbit)
self.assertEqual(response.content, b"Django is a magical pony!")
+ def test_pwd_change_custom_template(self):
+ self.client.login(username='super', password='secret')
+ su = User.objects.get(username='super')
+ try:
+ response = self.client.get('/test_admin/admin4/auth/user/%s/password/' % su.pk)
+ except TypeError:
+ self.fail('ModelAdmin.change_user_password_template should accept a list of template paths')
+ self.assertEqual(response.status_code, 200)
+
def get_perm(Model, perm):
"""Return the permission object, for the Model"""
@@ -3149,6 +3162,11 @@ class ReadonlyTest(TestCase):
self.assertContains(response, "Very awesome.")
self.assertContains(response, "Unkown coolness.")
self.assertContains(response, "foo")
+
+ # Checks that multiline text in a readonly field gets <br /> tags
+ self.assertContains(response, "Multiline<br />test<br />string")
+ self.assertContains(response, "InlineMultiline<br />test<br />string")
+
self.assertContains(response,
formats.localize(datetime.date.today() - datetime.timedelta(days=7))
)
@@ -3198,6 +3216,15 @@ class ReadonlyTest(TestCase):
response = self.client.get('/test_admin/admin2/auth/user/%s/password/' % su.pk)
self.assertEqual(response.status_code, 404)
+ def test_change_form_renders_correct_null_choice_value(self):
+ """
+ Regression test for #17911.
+ """
+ choice = Choice.objects.create(choice=None)
+ response = self.client.get('/test_admin/admin/admin_views/choice/%s/' % choice.pk)
+ self.assertContains(response, '<p>No opinion</p>', html=True)
+ self.assertNotContains(response, '<p>(None)</p>')
+
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class RawIdFieldsTest(TestCase):
@@ -3378,7 +3405,11 @@ class CSSTest(TestCase):
def tearDown(self):
self.client.logout()
- def test_css_classes(self):
+ def test_field_prefix_css_classes(self):
+ """
+ Ensure that fields have a CSS class name with a 'field-' prefix.
+ Refs #16371.
+ """
response = self.client.get('/test_admin/admin/admin_views/post/add/')
# The main form
@@ -3394,6 +3425,23 @@ class CSSTest(TestCase):
self.assertContains(response, '<td class="field-url">')
self.assertContains(response, '<td class="field-posted">')
+ def test_index_css_classes(self):
+ """
+ Ensure that CSS class names are used for each app and model on the
+ admin index pages.
+ Refs #17050.
+ """
+ # General index page
+ response = self.client.get("/test_admin/admin/")
+ self.assertContains(response, '<div class="app-admin_views module">')
+ self.assertContains(response, '<tr class="model-actor">')
+ self.assertContains(response, '<tr class="model-album">')
+
+ # App index page
+ response = self.client.get("/test_admin/admin/admin_views/")
+ self.assertContains(response, '<div class="app-admin_views module">')
+ self.assertContains(response, '<tr class="model-actor">')
+ self.assertContains(response, '<tr class="model-album">')
try:
import docutils
@@ -3693,3 +3741,61 @@ class AdminViewLogoutTest(TestCase):
self.assertEqual(response.template_name, 'admin/login.html')
self.assertEqual(response.request['PATH_INFO'], '/test_admin/admin/')
self.assertContains(response, '<input type="hidden" name="next" value="/test_admin/admin/" />')
+
+
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
+class AdminUserMessageTest(TestCase):
+ urls = "regressiontests.admin_views.urls"
+ fixtures = ['admin-views-users.xml']
+
+ def setUp(self):
+ self.client.login(username='super', password='secret')
+
+ def tearDown(self):
+ self.client.logout()
+
+ def send_message(self, level):
+ """
+ Helper that sends a post to the dummy test methods and asserts that a
+ message with the level has appeared in the response.
+ """
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ 'action': 'message_%s' % level,
+ 'index': 0,
+ }
+
+ response = self.client.post('/test_admin/admin/admin_views/usermessenger/',
+ action_data, follow=True)
+ self.assertContains(response,
+ '<li class="%s">Test %s</li>' % (level, level),
+ html=True)
+
+ @override_settings(MESSAGE_LEVEL=10) # Set to DEBUG for this request
+ def test_message_debug(self):
+ self.send_message('debug')
+
+ def test_message_info(self):
+ self.send_message('info')
+
+ def test_message_success(self):
+ self.send_message('success')
+
+ def test_message_warning(self):
+ self.send_message('warning')
+
+ def test_message_error(self):
+ self.send_message('error')
+
+ def test_message_extra_tags(self):
+ action_data = {
+ ACTION_CHECKBOX_NAME: [1],
+ 'action': 'message_extra_tags',
+ 'index': 0,
+ }
+
+ response = self.client.post('/test_admin/admin/admin_views/usermessenger/',
+ action_data, follow=True)
+ self.assertContains(response,
+ '<li class="extra_tag info">Test tags</li>',
+ html=True)
diff --git a/tests/regressiontests/admin_views/urls.py b/tests/regressiontests/admin_views/urls.py
index da6e2cbcf9..441834cd2a 100644
--- a/tests/regressiontests/admin_views/urls.py
+++ b/tests/regressiontests/admin_views/urls.py
@@ -11,4 +11,5 @@ urlpatterns = patterns('',
(r'^test_admin/admin/', include(admin.site.urls)),
(r'^test_admin/admin2/', include(customadmin.site.urls)),
(r'^test_admin/admin3/', include(admin.site.urls), dict(form_url='pony')),
+ (r'^test_admin/admin4/', include(customadmin.simple_site.urls)),
)
diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
index 2e452798b7..2977b86f3e 100644
--- a/tests/regressiontests/admin_widgets/models.py
+++ b/tests/regressiontests/admin_widgets/models.py
@@ -20,6 +20,7 @@ class Member(models.Model):
@python_2_unicode_compatible
class Band(models.Model):
name = models.CharField(max_length=100)
+ style = models.CharField(max_length=20)
members = models.ManyToManyField(Member)
def __str__(self):
diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
index 4b115431c1..76aa9f448f 100644
--- a/tests/regressiontests/admin_widgets/tests.py
+++ b/tests/regressiontests/admin_widgets/tests.py
@@ -10,7 +10,7 @@ from django.contrib.admin import widgets
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import SimpleUploadedFile
-from django.db.models import DateField
+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 translation
@@ -112,6 +112,23 @@ class AdminFormfieldForDBFieldTests(TestCase):
self.assertFormfield(models.Event, 'start_date', forms.TextInput,
formfield_overrides={DateField: {'widget': forms.TextInput}})
+ def testFormfieldOverridesWidgetInstances(self):
+ """
+ Test that widget instances in formfield_overrides are not shared between
+ different fields. (#19423)
+ """
+ class BandAdmin(admin.ModelAdmin):
+ formfield_overrides = {
+ CharField: {'widget': forms.TextInput(attrs={'size':'10'})}
+ }
+ ma = BandAdmin(models.Band, admin.site)
+ f1 = ma.formfield_for_dbfield(models.Band._meta.get_field('name'), request=None)
+ f2 = ma.formfield_for_dbfield(models.Band._meta.get_field('style'), request=None)
+ self.assertNotEqual(f1.widget, f2.widget)
+ self.assertEqual(f1.widget.attrs['maxlength'], '100')
+ self.assertEqual(f2.widget.attrs['maxlength'], '20')
+ self.assertEqual(f2.widget.attrs['size'], '10')
+
def testFieldWithChoices(self):
self.assertFormfield(models.Member, 'gender', forms.Select)
@@ -266,6 +283,37 @@ class AdminSplitDateTimeWidgetTest(DjangoTestCase):
)
+class AdminURLWidgetTest(DjangoTestCase):
+ def test_render(self):
+ w = widgets.AdminURLFieldWidget()
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', '')),
+ '<input class="vURLField" name="test" type="text" />'
+ )
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', 'http://example.com')),
+ '<p class="url">Currently:<a href="http://example.com">http://example.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com" /></p>'
+ )
+
+ def test_render_idn(self):
+ w = widgets.AdminURLFieldWidget()
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', 'http://example-äüö.com')),
+ '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com">http://example-äüö.com</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com" /></p>'
+ )
+
+ def test_render_quoting(self):
+ w = widgets.AdminURLFieldWidget()
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', 'http://example.com/<sometag>some text</sometag>')),
+ '<p class="url">Currently:<a href="http://example.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example.com/<sometag>some text</sometag>" /></p>'
+ )
+ self.assertHTMLEqual(
+ conditional_escape(w.render('test', 'http://example-äüö.com/<sometag>some text</sometag>')),
+ '<p class="url">Currently:<a href="http://xn--example--7za4pnc.com/%3Csometag%3Esome%20text%3C/sometag%3E">http://example-äüö.com/&lt;sometag&gt;some text&lt;/sometag&gt;</a><br />Change:<input class="vURLField" name="test" type="text" value="http://example-äüö.com/<sometag>some text</sometag>" /></p>'
+ )
+
+
class AdminFileWidgetTest(DjangoTestCase):
def test_render(self):
band = models.Band.objects.create(name='Linkin Park')
diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py
index af0f421502..596ebbfaec 100644
--- a/tests/regressiontests/aggregation_regress/tests.py
+++ b/tests/regressiontests/aggregation_regress/tests.py
@@ -470,7 +470,7 @@ class AggregationTests(TestCase):
# Regression for #15709 - Ensure each group_by field only exists once
# per query
qs = Book.objects.values('publisher').annotate(max_pages=Max('pages')).order_by()
- grouping, gb_params = qs.query.get_compiler(qs.db).get_grouping()
+ grouping, gb_params = qs.query.get_compiler(qs.db).get_grouping([])
self.assertEqual(len(grouping), 1)
def test_duplicate_alias(self):
@@ -889,3 +889,96 @@ class AggregationTests(TestCase):
self.assertIs(qs.query.alias_map['aggregation_regress_book'].join_type, None)
# Check that the query executes without problems.
self.assertEqual(len(qs.exclude(publisher=-1)), 6)
+
+ @skipUnlessDBFeature("allows_group_by_pk")
+ def test_aggregate_duplicate_columns(self):
+ # Regression test for #17144
+
+ results = Author.objects.annotate(num_contacts=Count('book_contact_set'))
+
+ # There should only be one GROUP BY clause, for the `id` column.
+ # `name` and `age` should not be grouped on.
+ grouping, gb_params = results.query.get_compiler(using='default').get_grouping([])
+ self.assertEqual(len(grouping), 1)
+ assert 'id' in grouping[0]
+ assert 'name' not in grouping[0]
+ assert 'age' not in grouping[0]
+
+ # The query group_by property should also only show the `id`.
+ self.assertEqual(results.query.group_by, [('aggregation_regress_author', 'id')])
+
+ # Ensure that we get correct results.
+ self.assertEqual(
+ [(a.name, a.num_contacts) for a in results.order_by('name')],
+ [
+ ('Adrian Holovaty', 1),
+ ('Brad Dayley', 1),
+ ('Jacob Kaplan-Moss', 0),
+ ('James Bennett', 1),
+ ('Jeffrey Forcier', 1),
+ ('Paul Bissex', 0),
+ ('Peter Norvig', 2),
+ ('Stuart Russell', 0),
+ ('Wesley J. Chun', 0),
+ ]
+ )
+
+ @skipUnlessDBFeature("allows_group_by_pk")
+ def test_aggregate_duplicate_columns_only(self):
+ # Works with only() too.
+ results = Author.objects.only('id', 'name').annotate(num_contacts=Count('book_contact_set'))
+ grouping, gb_params = results.query.get_compiler(using='default').get_grouping([])
+ self.assertEqual(len(grouping), 1)
+ assert 'id' in grouping[0]
+ assert 'name' not in grouping[0]
+ assert 'age' not in grouping[0]
+
+ # The query group_by property should also only show the `id`.
+ self.assertEqual(results.query.group_by, [('aggregation_regress_author', 'id')])
+
+ # Ensure that we get correct results.
+ self.assertEqual(
+ [(a.name, a.num_contacts) for a in results.order_by('name')],
+ [
+ ('Adrian Holovaty', 1),
+ ('Brad Dayley', 1),
+ ('Jacob Kaplan-Moss', 0),
+ ('James Bennett', 1),
+ ('Jeffrey Forcier', 1),
+ ('Paul Bissex', 0),
+ ('Peter Norvig', 2),
+ ('Stuart Russell', 0),
+ ('Wesley J. Chun', 0),
+ ]
+ )
+
+ @skipUnlessDBFeature("allows_group_by_pk")
+ def test_aggregate_duplicate_columns_select_related(self):
+ # And select_related()
+ results = Book.objects.select_related('contact').annotate(
+ num_authors=Count('authors'))
+ grouping, gb_params = results.query.get_compiler(using='default').get_grouping([])
+ self.assertEqual(len(grouping), 1)
+ assert 'id' in grouping[0]
+ assert 'name' not in grouping[0]
+ assert 'contact' not in grouping[0]
+
+ # The query group_by property should also only show the `id`.
+ self.assertEqual(results.query.group_by, [('aggregation_regress_book', 'id')])
+
+ # Ensure that we get correct results.
+ self.assertEqual(
+ [(b.name, b.num_authors) for b in results.order_by('name')],
+ [
+ ('Artificial Intelligence: A Modern Approach', 2),
+ ('Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp', 1),
+ ('Practical Django Projects', 1),
+ ('Python Web Development with Django', 3),
+ ('Sams Teach Yourself Django in 24 Hours', 1),
+ ('The Definitive Guide to Django: Web Development Done Right', 2)
+ ]
+ )
+
+ def test_reverse_join_trimming(self):
+ qs = Author.objects.annotate(Count('book_contact_set__contact'))
+ self.assertIn(' JOIN ', str(qs.query))
diff --git a/tests/regressiontests/app_loading/tests.py b/tests/regressiontests/app_loading/tests.py
index 0e66a5aad3..6dd0be2194 100644
--- a/tests/regressiontests/app_loading/tests.py
+++ b/tests/regressiontests/app_loading/tests.py
@@ -7,13 +7,14 @@ import time
from django.conf import Settings
from django.db.models.loading import cache, load_app, get_model, get_models
+from django.utils._os import upath
from django.utils.unittest import TestCase
class EggLoadingTest(TestCase):
def setUp(self):
self.old_path = sys.path[:]
- self.egg_dir = '%s/eggs' % os.path.dirname(__file__)
+ self.egg_dir = '%s/eggs' % os.path.dirname(upath(__file__))
# This test adds dummy applications to the app cache. These
# need to be removed in order to prevent bad interactions
diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py
index d284cfaac8..934c37d147 100644
--- a/tests/regressiontests/backends/tests.py
+++ b/tests/regressiontests/backends/tests.py
@@ -23,9 +23,28 @@ from django.utils import unittest
from . import models
+class DummyBackendTest(TestCase):
+ def test_no_databases(self):
+ """
+ Test that empty DATABASES setting default to the dummy backend.
+ """
+ DATABASES = {}
+ conns = ConnectionHandler(DATABASES)
+ self.assertEqual(conns[DEFAULT_DB_ALIAS].settings_dict['ENGINE'],
+ 'django.db.backends.dummy')
+
+
class OracleChecks(unittest.TestCase):
@unittest.skipUnless(connection.vendor == 'oracle',
+ "No need to check Oracle quote_name semantics")
+ def test_quote_name(self):
+ # Check that '%' chars are escaped for query execution.
+ name = '"SOME%NAME"'
+ quoted_name = connection.ops.quote_name(name)
+ self.assertEquals(quoted_name % (), name)
+
+ @unittest.skipUnless(connection.vendor == 'oracle',
"No need to check Oracle cursor semantics")
def test_dbms_session(self):
# If the backend is Oracle, test that we can call a standard
@@ -540,21 +559,30 @@ class ThreadTests(TestCase):
"""
connections_set = set()
connection.cursor()
- connections_set.add(connection.connection)
+ connections_set.add(connection)
def runner():
- from django.db import connection
+ # Passing django.db.connection between threads doesn't work while
+ # connections[DEFAULT_DB_ALIAS] does.
+ from django.db import connections
+ connection = connections[DEFAULT_DB_ALIAS]
+ # Allow thread sharing so the connection can be closed by the
+ # main thread.
+ connection.allow_thread_sharing = True
connection.cursor()
- connections_set.add(connection.connection)
+ connections_set.add(connection)
for x in range(2):
t = threading.Thread(target=runner)
t.start()
t.join()
- self.assertEqual(len(connections_set), 3)
+ # Check that each created connection got different inner connection.
+ self.assertEqual(
+ len(set([conn.connection for conn in connections_set])),
+ 3)
# Finish by closing the connections opened by the other threads (the
# connection opened in the main thread will automatically be closed on
# teardown).
for conn in connections_set:
- if conn != connection.connection:
+ if conn is not connection:
conn.close()
def test_connections_thread_local(self):
@@ -581,7 +609,7 @@ class ThreadTests(TestCase):
# connection opened in the main thread will automatically be closed on
# teardown).
for conn in connections_set:
- if conn != connection:
+ if conn is not connection:
conn.close()
def test_pass_connection_between_threads(self):
diff --git a/tests/regressiontests/bug639/tests.py b/tests/regressiontests/bug639/tests.py
index b7547696d4..fcc1e0f7d1 100644
--- a/tests/regressiontests/bug639/tests.py
+++ b/tests/regressiontests/bug639/tests.py
@@ -11,6 +11,7 @@ import shutil
from django.core.files.uploadedfile import SimpleUploadedFile
from django.utils import unittest
+from django.utils._os import upath
from .models import Photo, PhotoForm, temp_storage_dir
@@ -23,7 +24,7 @@ class Bug639Test(unittest.TestCase):
called.
"""
# Grab an image for testing.
- filename = os.path.join(os.path.dirname(__file__), "test.jpg")
+ filename = os.path.join(os.path.dirname(upath(__file__)), "test.jpg")
with open(filename, "rb") as fp:
img = fp.read()
diff --git a/tests/regressiontests/bulk_create/tests.py b/tests/regressiontests/bulk_create/tests.py
index 5d61242b9b..d4772934a1 100644
--- a/tests/regressiontests/bulk_create/tests.py
+++ b/tests/regressiontests/bulk_create/tests.py
@@ -82,6 +82,14 @@ class BulkCreateTests(TestCase):
with self.assertRaises(ValueError):
Country.objects.bulk_create([valid_country, invalid_country])
+ def test_batch_same_vals(self):
+ # Sqlite had a problem where all the same-valued models were
+ # collapsed to one insert.
+ Restaurant.objects.bulk_create([
+ Restaurant(name='foo') for i in range(0, 2)
+ ])
+ self.assertEqual(Restaurant.objects.count(), 2)
+
def test_large_batch(self):
with override_settings(DEBUG=True):
connection.queries = []
@@ -95,6 +103,14 @@ class BulkCreateTests(TestCase):
self.assertEqual(TwoFields.objects.filter(f2__gte=901).count(), 101)
@skipUnlessDBFeature('has_bulk_insert')
+ def test_large_single_field_batch(self):
+ # SQLite had a problem with more than 500 UNIONed selects in single
+ # query.
+ Restaurant.objects.bulk_create([
+ Restaurant() for i in range(0, 501)
+ ])
+
+ @skipUnlessDBFeature('has_bulk_insert')
def test_large_batch_efficiency(self):
with override_settings(DEBUG=True):
connection.queries = []
diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
index a6eff8950b..fc92ecf870 100644
--- a/tests/regressiontests/cache/tests.py
+++ b/tests/regressiontests/cache/tests.py
@@ -257,6 +257,7 @@ class BaseCacheTests(object):
self.assertEqual(self.cache.get('answer'), 42)
self.assertEqual(self.cache.incr('answer', 10), 52)
self.assertEqual(self.cache.get('answer'), 52)
+ self.assertEqual(self.cache.incr('answer', -10), 42)
self.assertRaises(ValueError, self.cache.incr, 'does_not_exist')
def test_decr(self):
@@ -266,8 +267,13 @@ class BaseCacheTests(object):
self.assertEqual(self.cache.get('answer'), 42)
self.assertEqual(self.cache.decr('answer', 10), 32)
self.assertEqual(self.cache.get('answer'), 32)
+ self.assertEqual(self.cache.decr('answer', -10), 42)
self.assertRaises(ValueError, self.cache.decr, 'does_not_exist')
+ def test_close(self):
+ self.assertTrue(hasattr(self.cache, 'close'))
+ self.cache.close()
+
def test_data_types(self):
# Many different data types can be cached
stuff = {
diff --git a/tests/regressiontests/comment_tests/tests/app_api_tests.py b/tests/regressiontests/comment_tests/tests/app_api_tests.py
index 8a63e8c188..a756068790 100644
--- a/tests/regressiontests/comment_tests/tests/app_api_tests.py
+++ b/tests/regressiontests/comment_tests/tests/app_api_tests.py
@@ -4,6 +4,9 @@ from django.conf import settings
from django.contrib import comments
from django.contrib.comments.models import Comment
from django.contrib.comments.forms import CommentForm
+from django.core.exceptions import ImproperlyConfigured
+from django.test.utils import override_settings
+from django.utils import six
from . import CommentTestCase
@@ -14,6 +17,14 @@ class CommentAppAPITests(CommentTestCase):
def testGetCommentApp(self):
self.assertEqual(comments.get_comment_app(), comments)
+ @override_settings(
+ COMMENTS_APP='missing_app',
+ INSTALLED_APPS=list(settings.INSTALLED_APPS) + ['missing_app'],
+ )
+ def testGetMissingCommentApp(self):
+ with six.assertRaisesRegex(self, ImproperlyConfigured, 'missing_app'):
+ _ = comments.get_comment_app()
+
def testGetForm(self):
self.assertEqual(comments.get_form(), CommentForm)
@@ -33,20 +44,14 @@ class CommentAppAPITests(CommentTestCase):
self.assertEqual(comments.get_approve_url(c), "/approve/12345/")
+@override_settings(
+ COMMENTS_APP='regressiontests.comment_tests.custom_comments',
+ INSTALLED_APPS=list(settings.INSTALLED_APPS) + [
+ 'regressiontests.comment_tests.custom_comments'],
+)
class CustomCommentTest(CommentTestCase):
urls = 'regressiontests.comment_tests.urls'
- def setUp(self):
- self.old_comments_app = getattr(settings, 'COMMENTS_APP', None)
- settings.COMMENTS_APP = 'regressiontests.comment_tests.custom_comments'
- settings.INSTALLED_APPS = list(settings.INSTALLED_APPS) + [settings.COMMENTS_APP,]
-
- def tearDown(self):
- del settings.INSTALLED_APPS[-1]
- settings.COMMENTS_APP = self.old_comments_app
- if settings.COMMENTS_APP is None:
- del settings._wrapped.COMMENTS_APP
-
def testGetCommentApp(self):
from regressiontests.comment_tests import custom_comments
self.assertEqual(comments.get_comment_app(), custom_comments)
diff --git a/tests/regressiontests/comment_tests/tests/comment_view_tests.py b/tests/regressiontests/comment_tests/tests/comment_view_tests.py
index 429f3b2bf2..5c1954026d 100644
--- a/tests/regressiontests/comment_tests/tests/comment_view_tests.py
+++ b/tests/regressiontests/comment_tests/tests/comment_view_tests.py
@@ -222,6 +222,13 @@ class CommentViewTests(CommentTestCase):
match = re.search(r"^http://testserver/somewhere/else/\?c=\d+$", location)
self.assertTrue(match != None, "Unexpected redirect location: %s" % location)
+ data["next"] = "http://badserver/somewhere/else/"
+ data["comment"] = "This is another comment with an unsafe next url"
+ response = self.client.post("/post/", data)
+ location = response["Location"]
+ match = post_redirect_re.match(location)
+ self.assertTrue(match != None, "Unsafe redirection to: %s" % location)
+
def testCommentDoneView(self):
a = Article.objects.get(pk=1)
data = self.getValidData(a)
diff --git a/tests/regressiontests/comment_tests/tests/feed_tests.py b/tests/regressiontests/comment_tests/tests/feed_tests.py
index 1ec316eab8..c93d7abf7d 100644
--- a/tests/regressiontests/comment_tests/tests/feed_tests.py
+++ b/tests/regressiontests/comment_tests/tests/feed_tests.py
@@ -1,5 +1,7 @@
from __future__ import absolute_import
+from xml.etree import ElementTree as ET
+
from django.conf import settings
from django.contrib.comments.models import Comment
from django.contrib.contenttypes.models import ContentType
@@ -31,8 +33,21 @@ class CommentFeedTests(CommentTestCase):
response = self.client.get(self.feed_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/rss+xml; charset=utf-8')
- self.assertContains(response, '<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">')
- self.assertContains(response, '<title>example.com comments</title>')
- self.assertContains(response, '<link>http://example.com/</link>')
- self.assertContains(response, '</rss>')
+
+ rss_elem = ET.fromstring(response.content)
+
+ self.assertEqual(rss_elem.tag, "rss")
+ self.assertEqual(rss_elem.attrib, {"version": "2.0"})
+
+ channel_elem = rss_elem.find("channel")
+
+ title_elem = channel_elem.find("title")
+ self.assertEqual(title_elem.text, "example.com comments")
+
+ link_elem = channel_elem.find("link")
+ self.assertEqual(link_elem.text, "http://example.com/")
+
+ atomlink_elem = channel_elem.find("{http://www.w3.org/2005/Atom}link")
+ self.assertEqual(atomlink_elem.attrib, {"href": "http://example.com/rss/comments/", "rel": "self"})
+
self.assertNotContains(response, "A comment for the second site.")
diff --git a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py
index 2ada20a662..0f83d5e210 100644
--- a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py
+++ b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py
@@ -4,7 +4,6 @@ from django.contrib.auth.models import User, Permission
from django.contrib.comments import signals
from django.contrib.comments.models import Comment, CommentFlag
from django.contrib.contenttypes.models import ContentType
-from django.conf import settings
from . import CommentTestCase
@@ -30,6 +29,30 @@ class FlagViewTests(CommentTestCase):
self.assertEqual(c.flags.filter(flag=CommentFlag.SUGGEST_REMOVAL).count(), 1)
return c
+ def testFlagPostNext(self):
+ """
+ POST the flag view, explicitly providing a next url.
+ """
+ comments = self.createSomeComments()
+ pk = comments[0].pk
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/flag/%d/" % pk, {'next': "/go/here/"})
+ self.assertEqual(response["Location"],
+ "http://testserver/go/here/?c=%d" % pk)
+
+ def testFlagPostUnsafeNext(self):
+ """
+ POSTing to the flag view with an unsafe next url will ignore the
+ provided url when redirecting.
+ """
+ comments = self.createSomeComments()
+ pk = comments[0].pk
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/flag/%d/" % pk,
+ {'next': "http://elsewhere/bad"})
+ self.assertEqual(response["Location"],
+ "http://testserver/flagged/?c=%d" % pk)
+
def testFlagPostTwice(self):
"""Users don't get to flag comments more than once."""
c = self.testFlagPost()
@@ -49,7 +72,7 @@ class FlagViewTests(CommentTestCase):
def testFlaggedView(self):
comments = self.createSomeComments()
pk = comments[0].pk
- response = self.client.get("/flagged/", data={"c":pk})
+ response = self.client.get("/flagged/", data={"c": pk})
self.assertTemplateUsed(response, "comments/flagged.html")
def testFlagSignals(self):
@@ -101,6 +124,33 @@ class DeleteViewTests(CommentTestCase):
self.assertTrue(c.is_removed)
self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_DELETION, user__username="normaluser").count(), 1)
+ def testDeletePostNext(self):
+ """
+ POSTing the delete view will redirect to an explicitly provided a next
+ url.
+ """
+ comments = self.createSomeComments()
+ pk = comments[0].pk
+ makeModerator("normaluser")
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/delete/%d/" % pk, {'next': "/go/here/"})
+ self.assertEqual(response["Location"],
+ "http://testserver/go/here/?c=%d" % pk)
+
+ def testDeletePostUnsafeNext(self):
+ """
+ POSTing to the delete view with an unsafe next url will ignore the
+ provided url when redirecting.
+ """
+ comments = self.createSomeComments()
+ pk = comments[0].pk
+ makeModerator("normaluser")
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/delete/%d/" % pk,
+ {'next': "http://elsewhere/bad"})
+ self.assertEqual(response["Location"],
+ "http://testserver/deleted/?c=%d" % pk)
+
def testDeleteSignals(self):
def receive(sender, **kwargs):
received_signals.append(kwargs.get('signal'))
@@ -116,13 +166,13 @@ class DeleteViewTests(CommentTestCase):
def testDeletedView(self):
comments = self.createSomeComments()
pk = comments[0].pk
- response = self.client.get("/deleted/", data={"c":pk})
+ response = self.client.get("/deleted/", data={"c": pk})
self.assertTemplateUsed(response, "comments/deleted.html")
class ApproveViewTests(CommentTestCase):
def testApprovePermissions(self):
- """The delete view should only be accessible to 'moderators'"""
+ """The approve view should only be accessible to 'moderators'"""
comments = self.createSomeComments()
pk = comments[0].pk
self.client.login(username="normaluser", password="normaluser")
@@ -134,7 +184,7 @@ class ApproveViewTests(CommentTestCase):
self.assertEqual(response.status_code, 200)
def testApprovePost(self):
- """POSTing the delete view should mark the comment as removed"""
+ """POSTing the approve view should mark the comment as removed"""
c1, c2, c3, c4 = self.createSomeComments()
c1.is_public = False; c1.save()
@@ -146,6 +196,36 @@ class ApproveViewTests(CommentTestCase):
self.assertTrue(c.is_public)
self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_APPROVAL, user__username="normaluser").count(), 1)
+ def testApprovePostNext(self):
+ """
+ POSTing the approve view will redirect to an explicitly provided a next
+ url.
+ """
+ c1, c2, c3, c4 = self.createSomeComments()
+ c1.is_public = False; c1.save()
+
+ makeModerator("normaluser")
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/approve/%d/" % c1.pk,
+ {'next': "/go/here/"})
+ self.assertEqual(response["Location"],
+ "http://testserver/go/here/?c=%d" % c1.pk)
+
+ def testApprovePostUnsafeNext(self):
+ """
+ POSTing to the approve view with an unsafe next url will ignore the
+ provided url when redirecting.
+ """
+ c1, c2, c3, c4 = self.createSomeComments()
+ c1.is_public = False; c1.save()
+
+ makeModerator("normaluser")
+ self.client.login(username="normaluser", password="normaluser")
+ response = self.client.post("/approve/%d/" % c1.pk,
+ {'next': "http://elsewhere/bad"})
+ self.assertEqual(response["Location"],
+ "http://testserver/approved/?c=%d" % c1.pk)
+
def testApproveSignals(self):
def receive(sender, **kwargs):
received_signals.append(kwargs.get('signal'))
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index bdf3c867c6..8596f8c801 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/tests/regressiontests/defaultfilters/tests.py
@@ -80,13 +80,16 @@ class DefaultFiltersTests(TestCase):
decimal_ctx.prec = old_prec
- # This fails because of Python's float handling. Floats with many zeroes
- # after the decimal point should be passed in as another type such as
- # unicode or Decimal.
- @unittest.expectedFailure
- def test_floatformat_fail(self):
+ def test_floatformat_py2_fail(self):
self.assertEqual(floatformat(1.00000000000000015, 16), '1.0000000000000002')
+ # The test above fails because of Python 2's float handling. Floats with
+ # many zeroes after the decimal point should be passed in as another type
+ # such as unicode or Decimal.
+ if not six.PY3:
+ test_floatformat_py2_fail = unittest.expectedFailure(test_floatformat_py2_fail)
+
+
def test_addslashes(self):
self.assertEqual(addslashes('"double quotes" and \'single quotes\''),
'\\"double quotes\\" and \\\'single quotes\\\'')
@@ -301,7 +304,16 @@ class DefaultFiltersTests(TestCase):
# Check urlize trims trailing period when followed by parenthesis - see #18644
self.assertEqual(urlize('(Go to http://www.example.com/foo.)'),
- '(Go to <a href="http://www.example.com/foo" rel="nofollow">http://www.example.com/foo</a>.)')
+ '(Go to <a href="http://www.example.com/foo" rel="nofollow">http://www.example.com/foo</a>.)')
+
+ # Check urlize doesn't crash when square bracket is appended to url (#19070)
+ self.assertEqual(urlize('[see www.example.com]'),
+ '[see <a href="http://www.example.com" rel="nofollow">www.example.com</a>]' )
+
+ # Check urlize doesn't crash when square bracket is prepended to url (#19070)
+ self.assertEqual(urlize('see test[at[example.com'),
+ 'see <a href="http://test[at[example.com" rel="nofollow">test[at[example.com</a>' )
+
def test_wordcount(self):
self.assertEqual(wordcount(''), 0)
diff --git a/tests/regressiontests/defer_regress/models.py b/tests/regressiontests/defer_regress/models.py
index 444649ac92..7528e3b2c9 100644
--- a/tests/regressiontests/defer_regress/models.py
+++ b/tests/regressiontests/defer_regress/models.py
@@ -55,6 +55,10 @@ class Feature(models.Model):
class SpecialFeature(models.Model):
feature = models.ForeignKey(Feature)
+class OneToOneItem(models.Model):
+ item = models.OneToOneField(Item, related_name="one_to_one_item")
+ name = models.CharField(max_length=15)
+
class ItemAndSimpleItem(models.Model):
item = models.ForeignKey(Item)
simple = models.ForeignKey(SimpleItem)
diff --git a/tests/regressiontests/defer_regress/tests.py b/tests/regressiontests/defer_regress/tests.py
index c77ca32135..d4d722035f 100644
--- a/tests/regressiontests/defer_regress/tests.py
+++ b/tests/regressiontests/defer_regress/tests.py
@@ -9,7 +9,7 @@ from django.db.models.loading import cache
from django.test import TestCase
from .models import (ResolveThis, Item, RelatedItem, Child, Leaf, Proxy,
- SimpleItem, Feature, ItemAndSimpleItem, SpecialFeature)
+ SimpleItem, Feature, ItemAndSimpleItem, OneToOneItem, SpecialFeature)
class DeferRegressionTest(TestCase):
@@ -111,6 +111,7 @@ class DeferRegressionTest(TestCase):
Item,
ItemAndSimpleItem,
Leaf,
+ OneToOneItem,
Proxy,
RelatedItem,
ResolveThis,
@@ -147,6 +148,7 @@ class DeferRegressionTest(TestCase):
"Leaf_Deferred_name_value",
"Leaf_Deferred_second_child_id_value",
"Leaf_Deferred_value",
+ "OneToOneItem",
"Proxy",
"RelatedItem",
"RelatedItem_Deferred_",
@@ -182,6 +184,33 @@ class DeferRegressionTest(TestCase):
self.assertEqual(1, qs.count())
self.assertEqual('Foobar', qs[0].name)
+ def test_reverse_one_to_one_relations(self):
+ # Refs #14694. Test reverse relations which are known unique (reverse
+ # side has o2ofield or unique FK) - the o2o case
+ item = Item.objects.create(name="first", value=42)
+ o2o = OneToOneItem.objects.create(item=item, name="second")
+ self.assertEqual(len(Item.objects.defer('one_to_one_item__name')), 1)
+ self.assertEqual(len(Item.objects.select_related('one_to_one_item')), 1)
+ self.assertEqual(len(Item.objects.select_related(
+ 'one_to_one_item').defer('one_to_one_item__name')), 1)
+ self.assertEqual(len(Item.objects.select_related('one_to_one_item').defer('value')), 1)
+ # Make sure that `only()` doesn't break when we pass in a unique relation,
+ # rather than a field on the relation.
+ self.assertEqual(len(Item.objects.only('one_to_one_item')), 1)
+ with self.assertNumQueries(1):
+ i = Item.objects.select_related('one_to_one_item')[0]
+ self.assertEqual(i.one_to_one_item.pk, o2o.pk)
+ self.assertEqual(i.one_to_one_item.name, "second")
+ with self.assertNumQueries(1):
+ i = Item.objects.select_related('one_to_one_item').defer(
+ 'value', 'one_to_one_item__name')[0]
+ self.assertEqual(i.one_to_one_item.pk, o2o.pk)
+ self.assertEqual(i.name, "first")
+ with self.assertNumQueries(1):
+ self.assertEqual(i.one_to_one_item.name, "second")
+ with self.assertNumQueries(1):
+ self.assertEqual(i.value, 42)
+
def test_defer_with_select_related(self):
item1 = Item.objects.create(name="first", value=47)
item2 = Item.objects.create(name="second", value=42)
diff --git a/tests/regressiontests/delete_regress/tests.py b/tests/regressiontests/delete_regress/tests.py
index f94bb2f20c..c3e4a6a0b7 100644
--- a/tests/regressiontests/delete_regress/tests.py
+++ b/tests/regressiontests/delete_regress/tests.py
@@ -3,7 +3,8 @@ from __future__ import absolute_import
import datetime
from django.conf import settings
-from django.db import backend, transaction, DEFAULT_DB_ALIAS, models
+from django.db import transaction, DEFAULT_DB_ALIAS, models
+from django.db.utils import ConnectionHandler
from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
from .models import (Book, Award, AwardNote, Person, Child, Toy, PlayedWith,
@@ -17,17 +18,8 @@ from .models import (Book, Award, AwardNote, Person, Child, Toy, PlayedWith,
class DeleteLockingTest(TransactionTestCase):
def setUp(self):
# Create a second connection to the default database
- conn_settings = settings.DATABASES[DEFAULT_DB_ALIAS]
- self.conn2 = backend.DatabaseWrapper({
- 'HOST': conn_settings['HOST'],
- 'NAME': conn_settings['NAME'],
- 'OPTIONS': conn_settings['OPTIONS'],
- 'PASSWORD': conn_settings['PASSWORD'],
- 'PORT': conn_settings['PORT'],
- 'USER': conn_settings['USER'],
- 'TIME_ZONE': settings.TIME_ZONE,
- })
-
+ new_connections = ConnectionHandler(settings.DATABASES)
+ self.conn2 = new_connections[DEFAULT_DB_ALIAS]
# Put both DB connections into managed transaction mode
transaction.enter_transaction_management()
transaction.managed(True)
diff --git a/tests/regressiontests/expressions_regress/tests.py b/tests/regressiontests/expressions_regress/tests.py
index 508a497151..ddb2c83b4f 100644
--- a/tests/regressiontests/expressions_regress/tests.py
+++ b/tests/regressiontests/expressions_regress/tests.py
@@ -26,12 +26,13 @@ class ExpressionsRegressTests(TestCase):
same object.
"""
self.assertQuerysetEqual(
- Number.objects.all(),
- [
- '<Number: -1, -1.000>',
- '<Number: 42, 42.000>',
- '<Number: 1337, 1337.000>'
- ]
+ Number.objects.all(),
+ [
+ '<Number: -1, -1.000>',
+ '<Number: 42, 42.000>',
+ '<Number: 1337, 1337.000>'
+ ],
+ ordered=False
)
def test_increment_value(self):
@@ -44,12 +45,13 @@ class ExpressionsRegressTests(TestCase):
2)
self.assertQuerysetEqual(
- Number.objects.all(),
- [
- '<Number: -1, -1.000>',
- '<Number: 43, 42.000>',
- '<Number: 1338, 1337.000>'
- ]
+ Number.objects.all(),
+ [
+ '<Number: -1, -1.000>',
+ '<Number: 43, 42.000>',
+ '<Number: 1338, 1337.000>'
+ ],
+ ordered=False
)
def test_filter_not_equals_other_field(self):
@@ -62,11 +64,12 @@ class ExpressionsRegressTests(TestCase):
.update(integer=F('integer') + 1),
2)
self.assertQuerysetEqual(
- Number.objects.exclude(float=F('integer')),
- [
- '<Number: 43, 42.000>',
- '<Number: 1338, 1337.000>'
- ]
+ Number.objects.exclude(float=F('integer')),
+ [
+ '<Number: 43, 42.000>',
+ '<Number: 1338, 1337.000>'
+ ],
+ ordered=False
)
def test_complex_expressions(self):
diff --git a/tests/regressiontests/extra_regress/tests.py b/tests/regressiontests/extra_regress/tests.py
index f591900afe..1bc6789edd 100644
--- a/tests/regressiontests/extra_regress/tests.py
+++ b/tests/regressiontests/extra_regress/tests.py
@@ -58,13 +58,15 @@ class ExtraRegressTests(TestCase):
('First Revision', 'First Revision'),
('Second Revision', 'First Revision'),
],
- transform=lambda r: (r.title, r.base.title)
+ transform=lambda r: (r.title, r.base.title),
+ ordered=False
)
# Following queryset should return the most recent revision:
self.assertQuerysetEqual(qs & qs2,
[('Second Revision', 'First Revision')],
- transform=lambda r: (r.title, r.base.title)
+ transform=lambda r: (r.title, r.base.title),
+ ordered=False
)
def test_extra_stay_tied(self):
@@ -342,5 +344,6 @@ class ExtraRegressTests(TestCase):
TestObject.objects.extra(
where=["first = 'a' OR second = 'a'", "third = 'a'"],
),
- ['<TestObject: TestObject: a,a,a>', '<TestObject: TestObject: b,a,a>']
+ ['<TestObject: TestObject: a,a,a>', '<TestObject: TestObject: b,a,a>'],
+ ordered=False
)
diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py
index 595b65d9f1..b6d3e1ff0b 100644
--- a/tests/regressiontests/file_storage/tests.py
+++ b/tests/regressiontests/file_storage/tests.py
@@ -24,6 +24,7 @@ from django.core.files.uploadedfile import UploadedFile
from django.test import SimpleTestCase
from django.utils import six
from django.utils import unittest
+from django.utils._os import upath
from django.test.utils import override_settings
from ..servers.tests import LiveServerBase
@@ -104,7 +105,7 @@ class FileStorageTests(unittest.TestCase):
"""
storage = self.storage_class(location='')
self.assertEqual(storage.base_location, '')
- self.assertEqual(storage.location, os.getcwd())
+ self.assertEqual(storage.location, upath(os.getcwd()))
def test_file_access_options(self):
"""
@@ -534,7 +535,7 @@ class DimensionClosingBug(unittest.TestCase):
from django.core.files import images
images.open = catching_open
try:
- get_image_dimensions(os.path.join(os.path.dirname(__file__), "test1.png"))
+ get_image_dimensions(os.path.join(os.path.dirname(upath(__file__)), "test1.png"))
finally:
del images.open
self.assertTrue(FileWrapper._closed)
@@ -551,7 +552,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase):
"""
from django.core.files.images import ImageFile
- img_path = os.path.join(os.path.dirname(__file__), "test.png")
+ img_path = os.path.join(os.path.dirname(upath(__file__)), "test.png")
image = ImageFile(open(img_path, 'rb'))
image_pil = Image.open(img_path)
size_1, size_2 = get_image_dimensions(image), get_image_dimensions(image)
@@ -560,6 +561,13 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase):
class ContentFileTestCase(unittest.TestCase):
+ def setUp(self):
+ self.storage_dir = tempfile.mkdtemp()
+ self.storage = FileSystemStorage(self.storage_dir)
+
+ def tearDown(self):
+ shutil.rmtree(self.storage_dir)
+
def test_content_file_default_name(self):
self.assertEqual(ContentFile(b"content").name, None)
@@ -576,7 +584,18 @@ class ContentFileTestCase(unittest.TestCase):
retrieved content is of the same type.
"""
self.assertTrue(isinstance(ContentFile(b"content").read(), bytes))
- self.assertTrue(isinstance(ContentFile("español").read(), six.text_type))
+ if six.PY3:
+ self.assertTrue(isinstance(ContentFile("español").read(), six.text_type))
+ else:
+ self.assertTrue(isinstance(ContentFile("español").read(), bytes))
+
+ def test_content_saving(self):
+ """
+ Test that ContentFile can be saved correctly with the filesystem storage,
+ both if it was initialized with string or unicode content"""
+ self.storage.save('bytes.txt', ContentFile(b"content"))
+ self.storage.save('unicode.txt', ContentFile("español"))
+
class NoNameFileTestCase(unittest.TestCase):
"""
diff --git a/tests/regressiontests/file_uploads/models.py b/tests/regressiontests/file_uploads/models.py
index 28e71c44bc..3336cc9d90 100644
--- a/tests/regressiontests/file_uploads/models.py
+++ b/tests/regressiontests/file_uploads/models.py
@@ -1,12 +1,5 @@
-import tempfile
-import os
-
-from django.core.files.storage import FileSystemStorage
from django.db import models
-temp_storage = FileSystemStorage(tempfile.mkdtemp())
-UPLOAD_TO = os.path.join(temp_storage.location, 'test_upload')
-
class FileModel(models.Model):
- testfile = models.FileField(storage=temp_storage, upload_to='test_upload')
+ testfile = models.FileField(upload_to='test_upload')
diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py
index 918f77d73c..45e7342abd 100644
--- a/tests/regressiontests/file_uploads/tests.py
+++ b/tests/regressiontests/file_uploads/tests.py
@@ -7,22 +7,36 @@ import hashlib
import json
import os
import shutil
+import tempfile as sys_tempfile
from django.core.files import temp as tempfile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http.multipartparser import MultiPartParser
from django.test import TestCase, client
+from django.test.utils import override_settings
from django.utils.encoding import force_bytes
from django.utils.six import StringIO
from django.utils import unittest
from . import uploadhandler
-from .models import FileModel, temp_storage, UPLOAD_TO
+from .models import FileModel
UNICODE_FILENAME = 'test-0123456789_中文_Orléans.jpg'
+MEDIA_ROOT = sys_tempfile.mkdtemp()
+UPLOAD_TO = os.path.join(MEDIA_ROOT, 'test_upload')
+@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class FileUploadTests(TestCase):
+ @classmethod
+ def setUpClass(cls):
+ if not os.path.isdir(MEDIA_ROOT):
+ os.makedirs(MEDIA_ROOT)
+
+ @classmethod
+ def tearDownClass(cls):
+ shutil.rmtree(MEDIA_ROOT)
+
def test_simple_upload(self):
with open(__file__, 'rb') as fp:
post_data = {
@@ -60,15 +74,14 @@ class FileUploadTests(TestCase):
self.assertEqual(response.status_code, 200)
- def test_base64_upload(self):
- test_string = "This data will be transmitted base64-encoded."
+ def _test_base64_upload(self, content):
payload = client.FakePayload("\r\n".join([
'--' + client.BOUNDARY,
'Content-Disposition: form-data; name="file"; filename="test.txt"',
'Content-Type: application/octet-stream',
'Content-Transfer-Encoding: base64',
'',]))
- payload.write(b"\r\n" + base64.b64encode(force_bytes(test_string)) + b"\r\n")
+ payload.write(b"\r\n" + base64.b64encode(force_bytes(content)) + b"\r\n")
payload.write('--' + client.BOUNDARY + '--\r\n')
r = {
'CONTENT_LENGTH': len(payload),
@@ -80,10 +93,17 @@ class FileUploadTests(TestCase):
response = self.client.request(**r)
received = json.loads(response.content.decode('utf-8'))
- self.assertEqual(received['file'], test_string)
+ self.assertEqual(received['file'], content)
+
+ def test_base64_upload(self):
+ self._test_base64_upload("This data will be transmitted base64-encoded.")
+
+ def test_big_base64_upload(self):
+ self._test_base64_upload("Big data" * 68000) # > 512Kb
def test_unicode_file_name(self):
- tdir = tempfile.gettempdir()
+ tdir = sys_tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, tdir, True)
# This file contains chinese symbols and an accented char in the name.
with open(os.path.join(tdir, UNICODE_FILENAME), 'w+b') as file1:
@@ -96,11 +116,6 @@ class FileUploadTests(TestCase):
response = self.client.post('/file_uploads/unicode_name/', post_data)
- try:
- os.unlink(file1.name)
- except OSError:
- pass
-
self.assertEqual(response.status_code, 200)
def test_dangerous_file_names(self):
@@ -347,26 +362,28 @@ class FileUploadTests(TestCase):
# shouldn't differ.
self.assertEqual(os.path.basename(obj.testfile.path), 'MiXeD_cAsE.txt')
-class DirectoryCreationTests(unittest.TestCase):
+@override_settings(MEDIA_ROOT=MEDIA_ROOT)
+class DirectoryCreationTests(TestCase):
"""
Tests for error handling during directory creation
via _save_FIELD_file (ticket #6450)
"""
+ @classmethod
+ def setUpClass(cls):
+ if not os.path.isdir(MEDIA_ROOT):
+ os.makedirs(MEDIA_ROOT)
+
+ @classmethod
+ def tearDownClass(cls):
+ shutil.rmtree(MEDIA_ROOT)
+
def setUp(self):
self.obj = FileModel()
- if not os.path.isdir(temp_storage.location):
- os.makedirs(temp_storage.location)
- if os.path.isdir(UPLOAD_TO):
- os.chmod(UPLOAD_TO, 0o700)
- shutil.rmtree(UPLOAD_TO)
-
- def tearDown(self):
- os.chmod(temp_storage.location, 0o700)
- shutil.rmtree(temp_storage.location)
def test_readonly_root(self):
"""Permission errors are not swallowed"""
- os.chmod(temp_storage.location, 0o500)
+ os.chmod(MEDIA_ROOT, 0o500)
+ self.addCleanup(os.chmod, MEDIA_ROOT, 0o700)
try:
self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', b'x'))
except OSError as err:
@@ -378,6 +395,7 @@ class DirectoryCreationTests(unittest.TestCase):
"""The correct IOError is raised when the upload directory name exists but isn't a directory"""
# Create a file with the upload directory name
open(UPLOAD_TO, 'wb').close()
+ self.addCleanup(os.remove, UPLOAD_TO)
with self.assertRaises(IOError) as exc_info:
self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', b'x'))
# The test needs to be done on a specific string as IOError
diff --git a/tests/regressiontests/file_uploads/views.py b/tests/regressiontests/file_uploads/views.py
index fcf32cecea..eb7b654c09 100644
--- a/tests/regressiontests/file_uploads/views.py
+++ b/tests/regressiontests/file_uploads/views.py
@@ -9,8 +9,8 @@ from django.http import HttpResponse, HttpResponseServerError
from django.utils import six
from django.utils.encoding import force_bytes
-from .models import FileModel, UPLOAD_TO
-from .tests import UNICODE_FILENAME
+from .models import FileModel
+from .tests import UNICODE_FILENAME, UPLOAD_TO
from .uploadhandler import QuotaUploadHandler, ErroringUploadHandler
diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py
index 55363bc5b7..988c5acd0c 100644
--- a/tests/regressiontests/fixtures_regress/tests.py
+++ b/tests/regressiontests/fixtures_regress/tests.py
@@ -14,6 +14,8 @@ from django.db.models import signals
from django.test import (TestCase, TransactionTestCase, skipIfDBFeature,
skipUnlessDBFeature)
from django.test.utils import override_settings
+from django.utils.encoding import force_text
+from django.utils._os import upath
from django.utils import six
from django.utils.six import PY3, StringIO
@@ -126,7 +128,7 @@ class TestFixtures(TestCase):
fixture directory.
"""
load_absolute_path = os.path.join(
- os.path.dirname(__file__),
+ os.path.dirname(upath(__file__)),
'fixtures',
'absolute.json'
)
@@ -388,7 +390,7 @@ class TestFixtures(TestCase):
commit=False,
)
- _cur_dir = os.path.dirname(os.path.abspath(__file__))
+ _cur_dir = os.path.dirname(os.path.abspath(upath(__file__)))
@override_settings(FIXTURE_DIRS=[os.path.join(_cur_dir, 'fixtures_1'),
os.path.join(_cur_dir, 'fixtures_2')])
@@ -430,7 +432,7 @@ class TestFixtures(TestCase):
stdout=stdout_output,
)
self.assertTrue("No xml fixture 'this_fixture_doesnt_exist' in" in
- stdout_output.getvalue())
+ force_text(stdout_output.getvalue()))
class NaturalKeyFixtureTests(TestCase):
diff --git a/tests/regressiontests/forms/models.py b/tests/regressiontests/forms/models.py
index 6e9c269356..bec31d12d7 100644
--- a/tests/regressiontests/forms/models.py
+++ b/tests/regressiontests/forms/models.py
@@ -83,4 +83,8 @@ class Group(models.Model):
class Cheese(models.Model):
- name = models.CharField(max_length=100)
+ name = models.CharField(max_length=100)
+
+
+class Article(models.Model):
+ content = models.TextField()
diff --git a/tests/regressiontests/forms/templates/forms/article_form.html b/tests/regressiontests/forms/templates/forms/article_form.html
new file mode 100644
index 0000000000..cde85051f6
--- /dev/null
+++ b/tests/regressiontests/forms/templates/forms/article_form.html
@@ -0,0 +1,8 @@
+<html>
+<body>
+ <form method="post" action=".">
+ {{ form.as_p }}<br>
+ <input id="submit" type="submit">
+ </form>
+</body>
+</html>
diff --git a/tests/regressiontests/forms/tests/__init__.py b/tests/regressiontests/forms/tests/__init__.py
index 8e2150cea3..6708e54c79 100644
--- a/tests/regressiontests/forms/tests/__init__.py
+++ b/tests/regressiontests/forms/tests/__init__.py
@@ -18,4 +18,4 @@ from .regressions import FormsRegressionsTestCase
from .util import FormsUtilTestCase
from .validators import TestFieldWithValidators
from .widgets import (FormsWidgetTestCase, FormsI18NWidgetsTestCase,
- WidgetTests, ClearableFileInputTests)
+ WidgetTests, LiveWidgetTests, ClearableFileInputTests)
diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py
index 1027afceb1..e17d976fcf 100644
--- a/tests/regressiontests/forms/tests/fields.py
+++ b/tests/regressiontests/forms/tests/fields.py
@@ -36,6 +36,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import *
from django.test import SimpleTestCase
from django.utils import six
+from django.utils._os import upath
def fix_os_paths(x):
@@ -928,12 +929,12 @@ class FieldsTests(SimpleTestCase):
# FilePathField ###############################################################
def test_filepathfield_1(self):
- path = os.path.abspath(forms.__file__)
+ path = os.path.abspath(upath(forms.__file__))
path = os.path.dirname(path) + '/'
self.assertTrue(fix_os_paths(path).endswith('/django/forms/'))
def test_filepathfield_2(self):
- path = forms.__file__
+ path = upath(forms.__file__)
path = os.path.dirname(os.path.abspath(path)) + '/'
f = FilePathField(path=path)
f.choices = [p for p in f.choices if p[0].endswith('.py')]
@@ -954,7 +955,7 @@ class FieldsTests(SimpleTestCase):
assert fix_os_paths(f.clean(path + 'fields.py')).endswith('/django/forms/fields.py')
def test_filepathfield_3(self):
- path = forms.__file__
+ path = upath(forms.__file__)
path = os.path.dirname(os.path.abspath(path)) + '/'
f = FilePathField(path=path, match='^.*?\.py$')
f.choices.sort()
@@ -972,7 +973,7 @@ class FieldsTests(SimpleTestCase):
self.assertTrue(got[0].endswith(exp[0]))
def test_filepathfield_4(self):
- path = os.path.abspath(forms.__file__)
+ path = os.path.abspath(upath(forms.__file__))
path = os.path.dirname(path) + '/'
f = FilePathField(path=path, recursive=True, match='^.*?\.py$')
f.choices.sort()
@@ -992,7 +993,7 @@ class FieldsTests(SimpleTestCase):
self.assertTrue(got[0].endswith(exp[0]))
def test_filepathfield_folders(self):
- path = os.path.dirname(__file__) + '/filepath_test_files/'
+ path = os.path.dirname(upath(__file__)) + '/filepath_test_files/'
f = FilePathField(path=path, allow_folders=True, allow_files=False)
f.choices.sort()
expected = [
diff --git a/tests/regressiontests/forms/tests/forms.py b/tests/regressiontests/forms/tests/forms.py
index a8a28ba806..1c83ed04d4 100644
--- a/tests/regressiontests/forms/tests/forms.py
+++ b/tests/regressiontests/forms/tests/forms.py
@@ -269,6 +269,11 @@ class FormsTestCase(TestCase):
f = SignupForm({'email': 'test@example.com', 'get_spam': 'false'}, auto_id=False)
self.assertHTMLEqual(str(f['get_spam']), '<input type="checkbox" name="get_spam" />')
+ # A value of '0' should be interpreted as a True value (#16820)
+ f = SignupForm({'email': 'test@example.com', 'get_spam': '0'})
+ self.assertTrue(f.is_valid())
+ self.assertTrue(f.cleaned_data.get('get_spam'))
+
def test_widget_output(self):
# Any Field can have a Widget class passed to its constructor:
class ContactForm(Form):
diff --git a/tests/regressiontests/forms/tests/formsets.py b/tests/regressiontests/forms/tests/formsets.py
index b3ceee551b..bf893c4c1d 100644
--- a/tests/regressiontests/forms/tests/formsets.py
+++ b/tests/regressiontests/forms/tests/formsets.py
@@ -856,6 +856,27 @@ class FormsFormsetTestCase(TestCase):
formset = FavoriteDrinksFormSet(error_class=CustomErrorList)
self.assertEqual(formset.forms[0].error_class, CustomErrorList)
+ def test_formset_calls_forms_is_valid(self):
+ # Regression tests for #18574 -- make sure formsets call
+ # is_valid() on each form.
+
+ class AnotherChoice(Choice):
+ def is_valid(self):
+ self.is_valid_called = True
+ return super(AnotherChoice, self).is_valid()
+
+ AnotherChoiceFormSet = formset_factory(AnotherChoice)
+ data = {
+ 'choices-TOTAL_FORMS': '1', # number of forms rendered
+ 'choices-INITIAL_FORMS': '0', # number of forms with initial data
+ 'choices-MAX_NUM_FORMS': '0', # max number of forms
+ 'choices-0-choice': 'Calexico',
+ 'choices-0-votes': '100',
+ }
+ formset = AnotherChoiceFormSet(data, auto_id=False, prefix='choices')
+ self.assertTrue(formset.is_valid())
+ self.assertTrue(all([form.is_valid_called for form in formset.forms]))
+
data = {
'choices-TOTAL_FORMS': '1', # the number of forms rendered
diff --git a/tests/regressiontests/forms/tests/input_formats.py b/tests/regressiontests/forms/tests/input_formats.py
index d591804b9d..7a305dbc2b 100644
--- a/tests/regressiontests/forms/tests/input_formats.py
+++ b/tests/regressiontests/forms/tests/input_formats.py
@@ -3,13 +3,14 @@ from datetime import time, date, datetime
from django import forms
from django.test.utils import override_settings
from django.utils.translation import activate, deactivate
-from django.utils.unittest import TestCase
+from django.test import SimpleTestCase
@override_settings(TIME_INPUT_FORMATS=["%I:%M:%S %p", "%I:%M %p"], USE_L10N=True)
-class LocalizedTimeTests(TestCase):
+class LocalizedTimeTests(SimpleTestCase):
def setUp(self):
- activate('de')
+ # nl/formats.py has customized TIME_INPUT_FORMATS
+ activate('nl')
def tearDown(self):
deactivate()
@@ -92,7 +93,7 @@ class LocalizedTimeTests(TestCase):
result = f.clean('13.30.05')
self.assertEqual(result, time(13,30,5))
- # # Check that the parsed result does a round trip to the same format
+ # Check that the parsed result does a round trip to the same format
text = f.widget._format_value(result)
self.assertEqual(text, "13:30:05")
@@ -106,7 +107,7 @@ class LocalizedTimeTests(TestCase):
@override_settings(TIME_INPUT_FORMATS=["%I:%M:%S %p", "%I:%M %p"])
-class CustomTimeInputFormatsTests(TestCase):
+class CustomTimeInputFormatsTests(SimpleTestCase):
def test_timeField(self):
"TimeFields can parse dates in the default format"
f = forms.TimeField()
@@ -198,7 +199,7 @@ class CustomTimeInputFormatsTests(TestCase):
self.assertEqual(text, "01:30:00 PM")
-class SimpleTimeFormatTests(TestCase):
+class SimpleTimeFormatTests(SimpleTestCase):
def test_timeField(self):
"TimeFields can parse dates in the default format"
f = forms.TimeField()
@@ -289,7 +290,7 @@ class SimpleTimeFormatTests(TestCase):
@override_settings(DATE_INPUT_FORMATS=["%d/%m/%Y", "%d-%m-%Y"], USE_L10N=True)
-class LocalizedDateTests(TestCase):
+class LocalizedDateTests(SimpleTestCase):
def setUp(self):
activate('de')
@@ -302,6 +303,9 @@ class LocalizedDateTests(TestCase):
# Parse a date in an unaccepted format; get an error
self.assertRaises(forms.ValidationError, f.clean, '21/12/2010')
+ # ISO formats are accepted, even if not specified in formats.py
+ self.assertEqual(f.clean('2010-12-21'), date(2010,12,21))
+
# Parse a date in a valid format, get a parsed result
result = f.clean('21.12.2010')
self.assertEqual(result, date(2010,12,21))
@@ -390,7 +394,7 @@ class LocalizedDateTests(TestCase):
@override_settings(DATE_INPUT_FORMATS=["%d.%m.%Y", "%d-%m-%Y"])
-class CustomDateInputFormatsTests(TestCase):
+class CustomDateInputFormatsTests(SimpleTestCase):
def test_dateField(self):
"DateFields can parse dates in the default format"
f = forms.DateField()
@@ -481,7 +485,7 @@ class CustomDateInputFormatsTests(TestCase):
text = f.widget._format_value(result)
self.assertEqual(text, "21.12.2010")
-class SimpleDateFormatTests(TestCase):
+class SimpleDateFormatTests(SimpleTestCase):
def test_dateField(self):
"DateFields can parse dates in the default format"
f = forms.DateField()
@@ -572,7 +576,7 @@ class SimpleDateFormatTests(TestCase):
@override_settings(DATETIME_INPUT_FORMATS=["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"], USE_L10N=True)
-class LocalizedDateTimeTests(TestCase):
+class LocalizedDateTimeTests(SimpleTestCase):
def setUp(self):
activate('de')
@@ -585,6 +589,9 @@ class LocalizedDateTimeTests(TestCase):
# Parse a date in an unaccepted format; get an error
self.assertRaises(forms.ValidationError, f.clean, '1:30:05 PM 21/12/2010')
+ # ISO formats are accepted, even if not specified in formats.py
+ self.assertEqual(f.clean('2010-12-21 13:30:05'), datetime(2010,12,21,13,30,5))
+
# Parse a date in a valid format, get a parsed result
result = f.clean('21.12.2010 13:30:05')
self.assertEqual(result, datetime(2010,12,21,13,30,5))
@@ -673,7 +680,7 @@ class LocalizedDateTimeTests(TestCase):
@override_settings(DATETIME_INPUT_FORMATS=["%I:%M:%S %p %d/%m/%Y", "%I:%M %p %d-%m-%Y"])
-class CustomDateTimeInputFormatsTests(TestCase):
+class CustomDateTimeInputFormatsTests(SimpleTestCase):
def test_dateTimeField(self):
"DateTimeFields can parse dates in the default format"
f = forms.DateTimeField()
@@ -764,7 +771,7 @@ class CustomDateTimeInputFormatsTests(TestCase):
text = f.widget._format_value(result)
self.assertEqual(text, "01:30:00 PM 21/12/2010")
-class SimpleDateTimeFormatTests(TestCase):
+class SimpleDateTimeFormatTests(SimpleTestCase):
def test_dateTimeField(self):
"DateTimeFields can parse dates in the default format"
f = forms.DateTimeField()
diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py
index 4bdd3f76ea..31c0e65c7c 100644
--- a/tests/regressiontests/forms/tests/widgets.py
+++ b/tests/regressiontests/forms/tests/widgets.py
@@ -4,7 +4,9 @@ from __future__ import unicode_literals
import copy
import datetime
+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
from django.core.files.uploadedfile import SimpleUploadedFile
+from django.core.urlresolvers import reverse
from django.forms import *
from django.forms.widgets import RadioFieldRenderer
from django.utils import formats
@@ -15,6 +17,8 @@ from django.test import TestCase
from django.test.utils import override_settings
from django.utils.encoding import python_2_unicode_compatible
+from ..models import Article
+
class FormsWidgetTestCase(TestCase):
# Each Widget class corresponds to an HTML form widget. A Widget knows how to
@@ -225,6 +229,10 @@ class FormsWidgetTestCase(TestCase):
# checkboxes).
self.assertFalse(w.value_from_datadict({}, {}, 'testing'))
+ value = w.value_from_datadict({'testing': '0'}, {}, 'testing')
+ self.assertIsInstance(value, bool)
+ self.assertTrue(value)
+
self.assertFalse(w._has_changed(None, None))
self.assertFalse(w._has_changed(None, ''))
self.assertFalse(w._has_changed('', None))
@@ -1091,6 +1099,22 @@ class WidgetTests(TestCase):
self.assertFalse(form.is_valid())
+class LiveWidgetTests(AdminSeleniumWebDriverTestCase):
+ urls = 'regressiontests.forms.urls'
+
+ def test_textarea_trailing_newlines(self):
+ """
+ Test that a roundtrip on a ModelForm doesn't alter the TextField value
+ """
+ article = Article.objects.create(content="\nTst\n")
+ self.selenium.get('%s%s' % (self.live_server_url,
+ reverse('article_form', args=[article.pk])))
+ self.selenium.find_element_by_id('submit').submit()
+ article = Article.objects.get(pk=article.pk)
+ # Should be "\nTst\n" after #19251 is fixed
+ self.assertEqual(article.content, "\r\nTst\r\n")
+
+
@python_2_unicode_compatible
class FakeFieldFile(object):
"""
diff --git a/tests/regressiontests/forms/urls.py b/tests/regressiontests/forms/urls.py
new file mode 100644
index 0000000000..b482b6c45c
--- /dev/null
+++ b/tests/regressiontests/forms/urls.py
@@ -0,0 +1,9 @@
+from django.conf.urls import patterns, url
+from django.views.generic.edit import UpdateView
+
+from .views import ArticleFormView
+
+
+urlpatterns = patterns('',
+ url(r'^/model_form/(?P<pk>\d+)/$', ArticleFormView.as_view(), name="article_form"),
+)
diff --git a/tests/regressiontests/forms/views.py b/tests/regressiontests/forms/views.py
new file mode 100644
index 0000000000..4bf384363c
--- /dev/null
+++ b/tests/regressiontests/forms/views.py
@@ -0,0 +1,8 @@
+from django.views.generic.edit import UpdateView
+
+from .models import Article
+
+
+class ArticleFormView(UpdateView):
+ model = Article
+ success_url = '/'
diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py
index 439b0d7327..c7ad7a0deb 100644
--- a/tests/regressiontests/generic_views/base.py
+++ b/tests/regressiontests/generic_views/base.py
@@ -216,6 +216,17 @@ class ViewTest(unittest.TestCase):
response_allows = set(response['Allow'].split(', '))
self.assertEqual(set(expected_methods + ('OPTIONS',)), response_allows)
+ def test_args_kwargs_request_on_self(self):
+ """
+ Test a view only has args, kwargs & request once `as_view`
+ has been called.
+ """
+ bare_view = InstanceView()
+ view = InstanceView.as_view()(self.rf.get('/'))
+ for attribute in ('args', 'kwargs', 'request'):
+ self.assertNotIn(attribute, dir(bare_view))
+ self.assertIn(attribute, dir(view))
+
class TemplateViewTest(TestCase):
urls = 'regressiontests.generic_views.urls'
diff --git a/tests/regressiontests/generic_views/edit.py b/tests/regressiontests/generic_views/edit.py
index 16f4da8efe..0f1eb3cca7 100644
--- a/tests/regressiontests/generic_views/edit.py
+++ b/tests/regressiontests/generic_views/edit.py
@@ -20,6 +20,15 @@ class FormMixinTests(TestCase):
initial_2 = FormMixin().get_initial()
self.assertNotEqual(initial_1, initial_2)
+
+class BasicFormTests(TestCase):
+ urls = 'regressiontests.generic_views.urls'
+
+ def test_post_data(self):
+ res = self.client.post('/contact/', {'name': "Me", 'message': "Hello"})
+ self.assertRedirects(res, 'http://testserver/list/authors/')
+
+
class ModelFormMixinTests(TestCase):
def test_get_form(self):
form_class = views.AuthorGetQuerySetFormView().get_form_class()
diff --git a/tests/regressiontests/generic_views/forms.py b/tests/regressiontests/generic_views/forms.py
index a78242f555..e036ad8afc 100644
--- a/tests/regressiontests/generic_views/forms.py
+++ b/tests/regressiontests/generic_views/forms.py
@@ -11,3 +11,8 @@ class AuthorForm(forms.ModelForm):
class Meta:
model = Author
+
+
+class ContactForm(forms.Form):
+ name = forms.CharField()
+ message = forms.CharField(widget=forms.Textarea)
diff --git a/tests/regressiontests/generic_views/list.py b/tests/regressiontests/generic_views/list.py
index 14dc1d725d..be2721d775 100644
--- a/tests/regressiontests/generic_views/list.py
+++ b/tests/regressiontests/generic_views/list.py
@@ -2,7 +2,9 @@ from __future__ import absolute_import
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
+from django.test.utils import override_settings
from django.views.generic.base import View
+from django.utils.encoding import force_str
from .models import Author, Artist
@@ -99,6 +101,16 @@ class ListViewTests(TestCase):
# Custom pagination allows for 2 orphans on a page size of 5
self.assertEqual(len(res.context['object_list']), 7)
+ def test_paginated_custom_page_kwarg(self):
+ self._make_authors(100)
+ res = self.client.get('/list/authors/paginated/custom_page_kwarg/', {'pagina': '2'})
+ self.assertEqual(res.status_code, 200)
+ self.assertTemplateUsed(res, 'generic_views/author_list.html')
+ self.assertEqual(len(res.context['object_list']), 30)
+ self.assertIs(res.context['author_list'], res.context['object_list'])
+ self.assertEqual(res.context['author_list'][0].name, 'Author 30')
+ self.assertEqual(res.context['page_obj'].number, 2)
+
def test_paginated_custom_paginator_constructor(self):
self._make_authors(7)
res = self.client.get('/list/authors/paginated/custom_constructor/')
@@ -106,8 +118,26 @@ class ListViewTests(TestCase):
# Custom pagination allows for 2 orphans on a page size of 5
self.assertEqual(len(res.context['object_list']), 7)
+ def test_paginated_orphaned_queryset(self):
+ self._make_authors(92)
+ res = self.client.get('/list/authors/paginated-orphaned/')
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['page_obj'].number, 1)
+ res = self.client.get(
+ '/list/authors/paginated-orphaned/', {'page': 'last'})
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['page_obj'].number, 3)
+ res = self.client.get(
+ '/list/authors/paginated-orphaned/', {'page': '3'})
+ self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.context['page_obj'].number, 3)
+ res = self.client.get(
+ '/list/authors/paginated-orphaned/', {'page': '4'})
+ self.assertEqual(res.status_code, 404)
+
def test_paginated_non_queryset(self):
res = self.client.get('/list/dict/paginated/')
+
self.assertEqual(res.status_code, 200)
self.assertEqual(len(res.context['object_list']), 1)
@@ -171,8 +201,17 @@ class ListViewTests(TestCase):
with self.assertNumQueries(3):
self.client.get('/list/authors/notempty/paginated/')
+ @override_settings(DEBUG=True)
+ def test_paginated_list_view_returns_useful_message_on_invalid_page(self):
+ # test for #19240
+ # tests that source exception's message is included in page
+ self._make_authors(1)
+ res = self.client.get('/list/authors/paginated/2/')
+ self.assertEqual(res.status_code, 404)
+ self.assertEqual(force_str(res.context.get('reason')),
+ "Invalid page (2): That page contains no results")
+
def _make_authors(self, n):
Author.objects.all().delete()
for i in range(n):
Author.objects.create(name='Author %02i' % i, slug='a%s' % i)
-
diff --git a/tests/regressiontests/generic_views/models.py b/tests/regressiontests/generic_views/models.py
index f59389ef78..30c73d67ca 100644
--- a/tests/regressiontests/generic_views/models.py
+++ b/tests/regressiontests/generic_views/models.py
@@ -1,3 +1,4 @@
+from django.core.urlresolvers import reverse
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
@@ -14,9 +15,8 @@ class Artist(models.Model):
def __str__(self):
return self.name
- @models.permalink
def get_absolute_url(self):
- return ('artist_detail', (), {'pk': self.id})
+ return reverse('artist_detail', kwargs={'pk': self.id})
@python_2_unicode_compatible
class Author(models.Model):
diff --git a/tests/regressiontests/generic_views/tests.py b/tests/regressiontests/generic_views/tests.py
index c985ad3309..3f80600efe 100644
--- a/tests/regressiontests/generic_views/tests.py
+++ b/tests/regressiontests/generic_views/tests.py
@@ -6,6 +6,6 @@ from .dates import (ArchiveIndexViewTests, YearArchiveViewTests,
MonthArchiveViewTests, WeekArchiveViewTests, DayArchiveViewTests,
DateDetailViewTests)
from .detail import DetailViewTest
-from .edit import (FormMixinTests, ModelFormMixinTests, CreateViewTests,
- UpdateViewTests, DeleteViewTests)
+from .edit import (FormMixinTests, BasicFormTests, ModelFormMixinTests,
+ CreateViewTests, UpdateViewTests, DeleteViewTests)
from .list import ListViewTests
diff --git a/tests/regressiontests/generic_views/urls.py b/tests/regressiontests/generic_views/urls.py
index c72bfecb65..34b37fa7c4 100644
--- a/tests/regressiontests/generic_views/urls.py
+++ b/tests/regressiontests/generic_views/urls.py
@@ -56,6 +56,10 @@ urlpatterns = patterns('',
(r'^detail/nonmodel/1/$',
views.NonModelDetail.as_view()),
+ # FormView
+ (r'^contact/$',
+ views.ContactView.as_view()),
+
# Create/UpdateView
(r'^edit/artists/create/$',
views.ArtistCreate.as_view()),
@@ -133,6 +137,8 @@ urlpatterns = patterns('',
views.AuthorList.as_view(paginate_by=30)),
(r'^list/authors/paginated/(?P<page>\d+)/$',
views.AuthorList.as_view(paginate_by=30)),
+ (r'^list/authors/paginated-orphaned/$',
+ views.AuthorList.as_view(paginate_by=30, paginate_orphans=2)),
(r'^list/authors/notempty/$',
views.AuthorList.as_view(allow_empty=False)),
(r'^list/authors/notempty/paginated/$',
@@ -149,6 +155,8 @@ urlpatterns = patterns('',
views.AuthorList.as_view(queryset=None)),
(r'^list/authors/paginated/custom_class/$',
views.AuthorList.as_view(paginate_by=5, paginator_class=views.CustomPaginator)),
+ (r'^list/authors/paginated/custom_page_kwarg/$',
+ views.AuthorList.as_view(paginate_by=30, page_kwarg='pagina')),
(r'^list/authors/paginated/custom_constructor/$',
views.AuthorListCustomPaginator.as_view()),
diff --git a/tests/regressiontests/generic_views/views.py b/tests/regressiontests/generic_views/views.py
index f7fcf6f509..71e78e82c7 100644
--- a/tests/regressiontests/generic_views/views.py
+++ b/tests/regressiontests/generic_views/views.py
@@ -2,11 +2,11 @@ from __future__ import absolute_import
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
-from django.core.urlresolvers import reverse
+from django.core.urlresolvers import reverse, reverse_lazy
from django.utils.decorators import method_decorator
from django.views import generic
-from .forms import AuthorForm
+from .forms import AuthorForm, ContactForm
from .models import Artist, Author, Book, Page, BookSigning
@@ -75,6 +75,13 @@ class AuthorListCustomPaginator(AuthorList):
orphans=2,
allow_empty_first_page=allow_empty_first_page)
+
+class ContactView(generic.FormView):
+ form_class = ContactForm
+ success_url = reverse_lazy('authors_list')
+ template_name = 'generic_views/form.html'
+
+
class ArtistCreate(generic.CreateView):
model = Artist
diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py
index 553b6ecff4..a601e541c6 100644
--- a/tests/regressiontests/httpwrappers/tests.py
+++ b/tests/regressiontests/httpwrappers/tests.py
@@ -14,6 +14,7 @@ from django.http import (QueryDict, HttpResponse, HttpResponseRedirect,
parse_cookie)
from django.test import TestCase
from django.utils.encoding import smart_str
+from django.utils._os import upath
from django.utils import six
from django.utils import unittest
@@ -149,7 +150,7 @@ class QueryDictTests(unittest.TestCase):
self.assertEqual(q.setdefault('foo', 'bar'), 'bar')
self.assertEqual(q['foo'], 'bar')
self.assertEqual(q.getlist('foo'), ['bar'])
- self.assertEqual(q.urlencode(), 'foo=bar&name=john')
+ self.assertIn(q.urlencode(), ['foo=bar&name=john', 'name=john&foo=bar'])
q.clear()
self.assertEqual(len(q), 0)
@@ -266,14 +267,18 @@ class HttpResponseTests(unittest.TestCase):
# The response also converts unicode or bytes keys to strings, but requires
# them to contain ASCII
r = HttpResponse()
+ del r['Content-Type']
r['foo'] = 'bar'
l = list(r.items())
+ self.assertEqual(len(l), 1)
self.assertEqual(l[0], ('foo', 'bar'))
self.assertIsInstance(l[0][0], str)
r = HttpResponse()
+ del r['Content-Type']
r[b'foo'] = 'bar'
l = list(r.items())
+ self.assertEqual(len(l), 1)
self.assertEqual(l[0], ('foo', 'bar'))
self.assertIsInstance(l[0][0], str)
@@ -479,7 +484,7 @@ class StreamingHttpResponseTests(TestCase):
class FileCloseTests(TestCase):
def test_response(self):
- filename = os.path.join(os.path.dirname(__file__), 'abc.txt')
+ filename = os.path.join(os.path.dirname(upath(__file__)), 'abc.txt')
# file isn't closed until we close the response.
file1 = open(filename)
@@ -512,7 +517,7 @@ class FileCloseTests(TestCase):
self.assertTrue(file2.closed)
def test_streaming_response(self):
- filename = os.path.join(os.path.dirname(__file__), 'abc.txt')
+ filename = os.path.join(os.path.dirname(upath(__file__)), 'abc.txt')
# file isn't closed until we close the response.
file1 = open(filename)
diff --git a/tests/regressiontests/i18n/commands/compilation.py b/tests/regressiontests/i18n/commands/compilation.py
index c6ab77941b..2944469110 100644
--- a/tests/regressiontests/i18n/commands/compilation.py
+++ b/tests/regressiontests/i18n/commands/compilation.py
@@ -4,9 +4,10 @@ from django.core.management import call_command, CommandError
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import translation, six
+from django.utils._os import upath
from django.utils.six import StringIO
-test_dir = os.path.abspath(os.path.dirname(__file__))
+test_dir = os.path.abspath(os.path.dirname(upath(__file__)))
class MessageCompilationTests(TestCase):
@@ -25,9 +26,9 @@ class PoFileTests(MessageCompilationTests):
def test_bom_rejection(self):
os.chdir(test_dir)
- with six.assertRaisesRegex(self, CommandError,
- "file has a BOM \(Byte Order Mark\)"):
+ with self.assertRaises(CommandError) as cm:
call_command('compilemessages', locale=self.LOCALE, stderr=StringIO())
+ self.assertIn("file has a BOM (Byte Order Mark)", cm.exception.args[0])
self.assertFalse(os.path.exists(self.MO_FILE))
diff --git a/tests/regressiontests/i18n/commands/extraction.py b/tests/regressiontests/i18n/commands/extraction.py
index ca2c3cc026..aa5efe1967 100644
--- a/tests/regressiontests/i18n/commands/extraction.py
+++ b/tests/regressiontests/i18n/commands/extraction.py
@@ -1,4 +1,5 @@
# -*- encoding: utf-8 -*-
+from __future__ import unicode_literals
import os
import re
@@ -6,6 +7,8 @@ import shutil
from django.core import management
from django.test import TestCase
+from django.utils.encoding import force_text
+from django.utils._os import upath
from django.utils.six import StringIO
@@ -17,7 +20,7 @@ class ExtractorTests(TestCase):
def setUp(self):
self._cwd = os.getcwd()
- self.test_dir = os.path.abspath(os.path.dirname(__file__))
+ self.test_dir = os.path.abspath(os.path.dirname(upath(__file__)))
def _rmrf(self, dname):
if os.path.commonprefix([self.test_dir, os.path.abspath(dname)]) != self.test_dir:
@@ -55,7 +58,7 @@ class BasicExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
self.assertTrue('#. Translators: This comment should be extracted' in po_contents)
self.assertTrue('This comment should not be extracted' not in po_contents)
# Comments in templates
@@ -83,7 +86,7 @@ class BasicExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
self.assertMsgId('Literal with a percent symbol at the end %%', po_contents)
self.assertMsgId('Literal with a percent %% symbol in the middle', po_contents)
self.assertMsgId('Completed 50%% of all the tasks', po_contents)
@@ -99,7 +102,7 @@ class BasicExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
self.assertMsgId('I think that 100%% is more that 50%% of anything.', po_contents)
self.assertMsgId('I think that 100%% is more that 50%% of %(obj)s.', po_contents)
self.assertMsgId("Blocktrans extraction shouldn't double escape this: %%, a=%(a)s", po_contents)
@@ -123,7 +126,7 @@ class BasicExtractorTests(ExtractorTests):
stdout = StringIO()
management.call_command('makemessages', locale=LOCALE, stdout=stdout)
os.remove('./code_sample.py')
- self.assertIn("code_sample.py:4", stdout.getvalue())
+ self.assertIn("code_sample.py:4", force_text(stdout.getvalue()))
def test_template_message_context_extractor(self):
"""
@@ -135,7 +138,7 @@ class BasicExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
# {% trans %}
self.assertTrue('msgctxt "Special trans context #1"' in po_contents)
self.assertTrue("Translatable literal #7a" in po_contents)
@@ -161,7 +164,7 @@ class BasicExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
# {% trans %}
self.assertTrue('msgctxt "Context wrapped in double quotes"' in po_contents)
self.assertTrue('msgctxt "Context wrapped in single quotes"' in po_contents)
@@ -216,7 +219,7 @@ class SymlinkExtractorTests(ExtractorTests):
def setUp(self):
self._cwd = os.getcwd()
- self.test_dir = os.path.abspath(os.path.dirname(__file__))
+ self.test_dir = os.path.abspath(os.path.dirname(upath(__file__)))
self.symlinked_dir = os.path.join(self.test_dir, 'templates_symlinked')
def tearDown(self):
@@ -238,7 +241,7 @@ class SymlinkExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0, symlinks=True)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
self.assertMsgId('This literal should be included.', po_contents)
self.assertTrue('templates_symlinked/test.html' in po_contents)
@@ -250,7 +253,7 @@ class CopyPluralFormsExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
self.assertTrue('Plural-Forms: nplurals=2; plural=(n != 1)' in po_contents)
@@ -261,7 +264,7 @@ class NoWrapExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0, no_wrap=True)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
self.assertMsgId('This literal should also be included wrapped or not wrapped depending on the use of the --no-wrap option.', po_contents)
def test_no_wrap_disabled(self):
@@ -269,7 +272,7 @@ class NoWrapExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0, no_wrap=False)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
self.assertMsgId('""\n"This literal should also be included wrapped or not wrapped depending on the "\n"use of the --no-wrap option."', po_contents, use_quotes=False)
@@ -280,7 +283,7 @@ class NoLocationExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0, no_location=True)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
self.assertFalse('#: templates/test.html:55' in po_contents)
def test_no_location_disabled(self):
@@ -288,5 +291,5 @@ class NoLocationExtractorTests(ExtractorTests):
management.call_command('makemessages', locale=LOCALE, verbosity=0, no_location=False)
self.assertTrue(os.path.exists(self.PO_FILE))
with open(self.PO_FILE, 'r') as fp:
- po_contents = fp.read()
+ po_contents = force_text(fp.read())
self.assertTrue('#: templates/test.html:55' in po_contents)
diff --git a/tests/regressiontests/i18n/contenttypes/tests.py b/tests/regressiontests/i18n/contenttypes/tests.py
index 178232f543..5e8a9823e1 100644
--- a/tests/regressiontests/i18n/contenttypes/tests.py
+++ b/tests/regressiontests/i18n/contenttypes/tests.py
@@ -6,6 +6,7 @@ import os
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from django.test.utils import override_settings
+from django.utils._os import upath
from django.utils import six
from django.utils import translation
@@ -13,7 +14,7 @@ from django.utils import translation
@override_settings(
USE_I18N=True,
LOCALE_PATHS=(
- os.path.join(os.path.dirname(__file__), 'locale'),
+ os.path.join(os.path.dirname(upath(__file__)), 'locale'),
),
LANGUAGE_CODE='en',
LANGUAGES=(
diff --git a/tests/regressiontests/i18n/patterns/tests.py b/tests/regressiontests/i18n/patterns/tests.py
index 73c9f56711..358cdf65db 100644
--- a/tests/regressiontests/i18n/patterns/tests.py
+++ b/tests/regressiontests/i18n/patterns/tests.py
@@ -7,16 +7,17 @@ from django.core.urlresolvers import reverse, clear_url_caches
from django.test import TestCase
from django.test.utils import override_settings
from django.template import Template, Context
+from django.utils._os import upath
from django.utils import translation
@override_settings(
USE_I18N=True,
LOCALE_PATHS=(
- os.path.join(os.path.dirname(__file__), 'locale'),
+ os.path.join(os.path.dirname(upath(__file__)), 'locale'),
),
TEMPLATE_DIRS=(
- os.path.join(os.path.dirname(__file__), 'templates'),
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
),
LANGUAGE_CODE='en',
LANGUAGES=(
diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py
index 4054f85ef0..dcc288e600 100644
--- a/tests/regressiontests/i18n/tests.py
+++ b/tests/regressiontests/i18n/tests.py
@@ -18,6 +18,7 @@ from django.utils.formats import (get_format, date_format, time_format,
number_format)
from django.utils.importlib import import_module
from django.utils.numberformat import format as nformat
+from django.utils._os import upath
from django.utils.safestring import mark_safe, SafeBytes, SafeString, SafeText
from django.utils import six
from django.utils.six import PY3
@@ -44,7 +45,7 @@ from .patterns.tests import (URLRedirectWithoutTrailingSlashTests,
URLPrefixTests, URLResponseTests, URLRedirectTests, PathUnusedTests)
-here = os.path.dirname(os.path.abspath(__file__))
+here = os.path.dirname(os.path.abspath(upath(__file__)))
extended_locale_paths = settings.LOCALE_PATHS + (
os.path.join(here, 'other', 'locale'),
)
@@ -83,6 +84,10 @@ class TranslationTests(TestCase):
s4 = ugettext_lazy('Some other string')
self.assertEqual(False, s == s4)
+ if not six.PY3:
+ # On Python 2, gettext_lazy should not transform a bytestring to unicode
+ self.assertEqual(gettext_lazy(b"test").upper(), b"TEST")
+
def test_lazy_pickle(self):
s1 = ugettext_lazy("test")
self.assertEqual(six.text_type(s1), "test")
@@ -662,8 +667,8 @@ class FormattingTests(TestCase):
with self.settings(USE_L10N=True,
FORMAT_MODULE_PATH='regressiontests.i18n.other.locale'):
with translation.override('de', deactivate=True):
- old = "%r" % get_format_modules(reverse=True)
- new = "%r" % get_format_modules(reverse=True) # second try
+ old = str("%r") % get_format_modules(reverse=True)
+ new = str("%r") % get_format_modules(reverse=True) # second try
self.assertEqual(new, old, 'Value returned by get_formats_modules() must be preserved between calls.')
def test_localize_templatetag_and_filter(self):
diff --git a/tests/regressiontests/indexes/__init__.py b/tests/regressiontests/indexes/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/indexes/__init__.py
diff --git a/tests/regressiontests/indexes/models.py b/tests/regressiontests/indexes/models.py
new file mode 100644
index 0000000000..9758377f99
--- /dev/null
+++ b/tests/regressiontests/indexes/models.py
@@ -0,0 +1,11 @@
+from django.db import models
+
+
+class Article(models.Model):
+ headline = models.CharField(max_length=100)
+ pub_date = models.DateTimeField()
+
+ class Meta:
+ index_together = [
+ ["headline", "pub_date"],
+ ]
diff --git a/tests/regressiontests/indexes/tests.py b/tests/regressiontests/indexes/tests.py
new file mode 100644
index 0000000000..0dac881fa9
--- /dev/null
+++ b/tests/regressiontests/indexes/tests.py
@@ -0,0 +1,12 @@
+from django.core.management.color import no_style
+from django.db import connections, DEFAULT_DB_ALIAS
+from django.test import TestCase
+
+from .models import Article
+
+
+class IndexesTests(TestCase):
+ def test_index_together(self):
+ connection = connections[DEFAULT_DB_ALIAS]
+ index_sql = connection.creation.sql_indexes_for_model(Article, no_style())
+ self.assertEqual(len(index_sql), 1)
diff --git a/tests/regressiontests/initial_sql_regress/sql/simple.sql b/tests/regressiontests/initial_sql_regress/sql/simple.sql
index 39363baa9a..d82f8381af 100644
--- a/tests/regressiontests/initial_sql_regress/sql/simple.sql
+++ b/tests/regressiontests/initial_sql_regress/sql/simple.sql
@@ -2,7 +2,8 @@
INSERT INTO initial_sql_regress_simple (name) VALUES ('John'); -- another comment
INSERT INTO initial_sql_regress_simple (name) VALUES ('-- Comment Man');
INSERT INTO initial_sql_regress_simple (name) VALUES ('Paul');
-INSERT INTO initial_sql_regress_simple (name) VALUES ('Ringo');
+INSERT INTO
+ initial_sql_regress_simple (name) VALUES ('Ringo');
INSERT INTO initial_sql_regress_simple (name) VALUES ('George');
INSERT INTO initial_sql_regress_simple (name) VALUES ('Miles O''Brien');
INSERT INTO initial_sql_regress_simple (name) VALUES ('Semicolon;Man');
diff --git a/tests/regressiontests/initial_sql_regress/tests.py b/tests/regressiontests/initial_sql_regress/tests.py
index 03a91cb807..39d8921061 100644
--- a/tests/regressiontests/initial_sql_regress/tests.py
+++ b/tests/regressiontests/initial_sql_regress/tests.py
@@ -1,3 +1,6 @@
+from django.core.management.color import no_style
+from django.core.management.sql import custom_sql_for_model
+from django.db import connections, DEFAULT_DB_ALIAS
from django.test import TestCase
from .models import Simple
@@ -15,10 +18,6 @@ class InitialSQLTests(TestCase):
self.assertEqual(Simple.objects.count(), 0)
def test_custom_sql(self):
- from django.core.management.sql import custom_sql_for_model
- from django.core.management.color import no_style
- from django.db import connections, DEFAULT_DB_ALIAS
-
# Simulate the custom SQL loading by syncdb
connection = connections[DEFAULT_DB_ALIAS]
custom_sql = custom_sql_for_model(Simple, no_style(), connection)
diff --git a/tests/regressiontests/inspectdb/tests.py b/tests/regressiontests/inspectdb/tests.py
index 484e7f4060..028d263337 100644
--- a/tests/regressiontests/inspectdb/tests.py
+++ b/tests/regressiontests/inspectdb/tests.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
from django.core.management import call_command
+from django.db import connection
from django.test import TestCase, skipUnlessDBFeature
from django.utils.six import StringIO
@@ -60,14 +61,16 @@ class InspectDBTestCase(TestCase):
self.assertIn("number_45extra = models.CharField", output)
def test_special_column_name_introspection(self):
- """Introspection of column names containing special characters,
- unsuitable for Python identifiers
+ """
+ Introspection of column names containing special characters,
+ unsuitable for Python identifiers
"""
out = StringIO()
call_command('inspectdb', stdout=out)
output = out.getvalue()
+ base_name = 'Field' if connection.vendor != 'oracle' else 'field'
self.assertIn("field = models.IntegerField()", output)
- self.assertIn("field_field = models.IntegerField(db_column='Field_')", output)
- self.assertIn("field_field_0 = models.IntegerField(db_column='Field__')", output)
+ self.assertIn("field_field = models.IntegerField(db_column='%s_')" % base_name, output)
+ self.assertIn("field_field_0 = models.IntegerField(db_column='%s__')" % base_name, output)
self.assertIn("field_field_1 = models.IntegerField(db_column='__field')", output)
self.assertIn("prc_x = models.IntegerField(db_column='prc(%) x')", output)
diff --git a/tests/regressiontests/introspection/models.py b/tests/regressiontests/introspection/models.py
index 6e5beba61d..4de82e47e7 100644
--- a/tests/regressiontests/introspection/models.py
+++ b/tests/regressiontests/introspection/models.py
@@ -17,6 +17,7 @@ class Reporter(models.Model):
def __str__(self):
return "%s %s" % (self.first_name, self.last_name)
+
@python_2_unicode_compatible
class Article(models.Model):
headline = models.CharField(max_length=100)
@@ -28,3 +29,6 @@ class Article(models.Model):
class Meta:
ordering = ('headline',)
+ index_together = [
+ ["headline", "pub_date"],
+ ]
diff --git a/tests/regressiontests/introspection/tests.py b/tests/regressiontests/introspection/tests.py
index a54e0c670b..2df946d874 100644
--- a/tests/regressiontests/introspection/tests.py
+++ b/tests/regressiontests/introspection/tests.py
@@ -1,14 +1,19 @@
-from __future__ import absolute_import,unicode_literals
+from __future__ import absolute_import, unicode_literals
from functools import update_wrapper
from django.db import connection
from django.test import TestCase, skipUnlessDBFeature, skipIfDBFeature
-from django.utils import six
+from django.utils import six, unittest
from .models import Reporter, Article
-#
+if connection.vendor == 'oracle':
+ expectedFailureOnOracle = unittest.expectedFailure
+else:
+ expectedFailureOnOracle = lambda f: f
+
+
# The introspection module is optional, so methods tested here might raise
# NotImplementedError. This is perfectly acceptable behavior for the backend
# in question, but the tests need to handle this without failing. Ideally we'd
@@ -18,7 +23,7 @@ from .models import Reporter, Article
# wrapper that ignores the exception.
#
# The metaclass is just for fun.
-#
+
def ignore_not_implemented(func):
def _inner(*args, **kwargs):
@@ -29,15 +34,16 @@ def ignore_not_implemented(func):
update_wrapper(_inner, func)
return _inner
+
class IgnoreNotimplementedError(type):
def __new__(cls, name, bases, attrs):
- for k,v in attrs.items():
+ for k, v in attrs.items():
if k.startswith('test'):
attrs[k] = ignore_not_implemented(v)
return type.__new__(cls, name, bases, attrs)
-class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)):
+class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)):
def test_table_names(self):
tl = connection.introspection.table_names()
self.assertEqual(tl, sorted(tl))
@@ -89,7 +95,13 @@ class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)
[datatype(r[1], r) for r in desc],
['IntegerField', 'CharField', 'CharField', 'CharField', 'BigIntegerField']
)
- # Check also length of CharFields
+
+ # The following test fails on Oracle due to #17202 (can't correctly
+ # inspect the length of character columns).
+ @expectedFailureOnOracle
+ def test_get_table_description_col_lengths(self):
+ cursor = connection.cursor()
+ desc = connection.introspection.get_table_description(cursor, Reporter._meta.db_table)
self.assertEqual(
[r[3] for r in desc if datatype(r[1], r) == 'CharField'],
[30, 30, 75]
@@ -152,6 +164,7 @@ class IntrospectionTests(six.with_metaclass(IgnoreNotimplementedError, TestCase)
self.assertNotIn('first_name', indexes)
self.assertIn('id', indexes)
+
def datatype(dbtype, description):
"""Helper to convert a data type into a string."""
dt = connection.introspection.get_field_type(dbtype, description)
diff --git a/tests/regressiontests/logging_tests/logconfig.py b/tests/regressiontests/logging_tests/logconfig.py
index fc5ea1a0bd..b068103eb9 100644
--- a/tests/regressiontests/logging_tests/logconfig.py
+++ b/tests/regressiontests/logging_tests/logconfig.py
@@ -1,8 +1,15 @@
import logging
from django.conf import settings
+from django.core.mail.backends.base import BaseEmailBackend
+
class MyHandler(logging.Handler):
def __init__(self):
logging.Handler.__init__(self)
self.config = settings.LOGGING
+
+
+class MyEmailBackend(BaseEmailBackend):
+ def send_messages(self, email_messages):
+ pass
diff --git a/tests/regressiontests/logging_tests/tests.py b/tests/regressiontests/logging_tests/tests.py
index e40800efde..07804eb398 100644
--- a/tests/regressiontests/logging_tests/tests.py
+++ b/tests/regressiontests/logging_tests/tests.py
@@ -2,17 +2,23 @@ from __future__ import unicode_literals
import copy
import logging
+import sys
import warnings
-from django.conf import compat_patch_logging_config
+from django.conf import compat_patch_logging_config, LazySettings
from django.core import mail
from django.test import TestCase, RequestFactory
from django.test.utils import override_settings
+from django.utils.encoding import force_text
from django.utils.log import CallbackFilter, RequireDebugFalse
from django.utils.six import StringIO
+from django.utils.unittest import skipUnless
from ..admin_scripts.tests import AdminScriptTestCase
+from .logconfig import MyEmailBackend
+
+PYVERS = sys.version_info[:2]
# logging config prior to using filter with mail_admins
OLD_LOGGING = {
@@ -131,6 +137,32 @@ class DefaultLoggingTest(TestCase):
self.logger.error("Hey, this is an error.")
self.assertEqual(output.getvalue(), 'Hey, this is an error.\n')
+@skipUnless(PYVERS > (2,6), "warnings captured only in Python >= 2.7")
+class WarningLoggerTests(TestCase):
+ """
+ Tests that warnings output for DeprecationWarnings is enabled
+ and captured to the logging system
+ """
+ def setUp(self):
+ self.logger = logging.getLogger('py.warnings')
+ self.old_stream = self.logger.handlers[0].stream
+
+ def tearDown(self):
+ self.logger.handlers[0].stream = self.old_stream
+
+ @override_settings(DEBUG=True)
+ def test_warnings_capture(self):
+ output = StringIO()
+ self.logger.handlers[0].stream = output
+ warnings.warn('Foo Deprecated', DeprecationWarning)
+ self.assertTrue('Foo Deprecated' in force_text(output.getvalue()))
+
+ def test_warnings_capture_debug_false(self):
+ output = StringIO()
+ self.logger.handlers[0].stream = output
+ warnings.warn('Foo Deprecated', DeprecationWarning)
+ self.assertFalse('Foo Deprecated' in force_text(output.getvalue()))
+
class CallbackFilterTest(TestCase):
def test_sense(self):
@@ -276,6 +308,38 @@ class AdminEmailHandlerTest(TestCase):
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, expected_subject)
+ @override_settings(
+ ADMINS=(('admin', 'admin@example.com'),),
+ DEBUG=False,
+ )
+ def test_uses_custom_email_backend(self):
+ """
+ Refs #19325
+ """
+ message = 'All work and no play makes Jack a dull boy'
+ admin_email_handler = self.get_admin_email_handler(self.logger)
+ mail_admins_called = {'called': False}
+
+ def my_mail_admins(*args, **kwargs):
+ connection = kwargs['connection']
+ self.assertTrue(isinstance(connection, MyEmailBackend))
+ mail_admins_called['called'] = True
+
+ # Monkeypatches
+ orig_mail_admins = mail.mail_admins
+ orig_email_backend = admin_email_handler.email_backend
+ mail.mail_admins = my_mail_admins
+ admin_email_handler.email_backend = (
+ 'regressiontests.logging_tests.logconfig.MyEmailBackend')
+
+ try:
+ self.logger.error(message)
+ self.assertTrue(mail_admins_called['called'])
+ finally:
+ # Revert Monkeypatches
+ mail.mail_admins = orig_mail_admins
+ admin_email_handler.email_backend = orig_email_backend
+
class SettingsConfigTest(AdminScriptTestCase):
"""
@@ -302,3 +366,20 @@ class SettingsConfigTest(AdminScriptTestCase):
out, err = self.run_manage(['validate'])
self.assertNoOutput(err)
self.assertOutput(out, "0 errors found")
+
+
+def dictConfig(config):
+ dictConfig.called = True
+dictConfig.called = False
+
+
+class SettingsConfigureLogging(TestCase):
+ """
+ Test that calling settings.configure() initializes the logging
+ configuration.
+ """
+ def test_configure_initializes_logging(self):
+ settings = LazySettings()
+ settings.configure(
+ LOGGING_CONFIG='regressiontests.logging_tests.tests.dictConfig')
+ self.assertTrue(dictConfig.called)
diff --git a/tests/regressiontests/m2m_regress/models.py b/tests/regressiontests/m2m_regress/models.py
index 7c1108456e..1a4a6df354 100644
--- a/tests/regressiontests/m2m_regress/models.py
+++ b/tests/regressiontests/m2m_regress/models.py
@@ -61,3 +61,20 @@ class Worksheet(models.Model):
class User(models.Model):
name = models.CharField(max_length=30)
friends = models.ManyToManyField(auth.User)
+
+
+class BadModelWithSplit(models.Model):
+ name = models.CharField(max_length=1)
+
+ def split(self):
+ raise RuntimeError('split should not be called')
+
+ class Meta:
+ abstract = True
+
+
+class RegressionModelSplit(BadModelWithSplit):
+ """
+ Model with a split method should not cause an error in add_lazy_relation
+ """
+ others = models.ManyToManyField('self')
diff --git a/tests/regressiontests/m2m_regress/tests.py b/tests/regressiontests/m2m_regress/tests.py
index 92628c1825..3dc1d2417c 100644
--- a/tests/regressiontests/m2m_regress/tests.py
+++ b/tests/regressiontests/m2m_regress/tests.py
@@ -5,7 +5,7 @@ from django.test import TestCase
from django.utils import six
from .models import (SelfRefer, Tag, TagCollection, Entry, SelfReferChild,
- SelfReferChildSibling, Worksheet)
+ SelfReferChildSibling, Worksheet, RegressionModelSplit)
class M2MRegressionTests(TestCase):
@@ -74,7 +74,7 @@ class M2MRegressionTests(TestCase):
c1.tags = [t1, t2]
c1 = TagCollection.objects.get(name='c1')
- self.assertQuerysetEqual(c1.tags.all(), ["<Tag: t1>", "<Tag: t2>"])
+ self.assertQuerysetEqual(c1.tags.all(), ["<Tag: t1>", "<Tag: t2>"], ordered=False)
self.assertQuerysetEqual(t1.tag_collections.all(), ["<TagCollection: c1>"])
def test_manager_class_caching(self):
@@ -90,3 +90,9 @@ class M2MRegressionTests(TestCase):
# Get same manager for different instances
self.assertTrue(e1.topics.__class__ is e2.topics.__class__)
self.assertTrue(t1.entry_set.__class__ is t2.entry_set.__class__)
+
+ def test_m2m_abstract_split(self):
+ # Regression for #19236 - an abstract class with a 'split' method
+ # causes a TypeError in add_lazy_relation
+ m1 = RegressionModelSplit(name='1')
+ m1.save()
diff --git a/tests/regressiontests/m2m_through_regress/models.py b/tests/regressiontests/m2m_through_regress/models.py
index 47c24ed5b2..91e1aa8cc1 100644
--- a/tests/regressiontests/m2m_through_regress/models.py
+++ b/tests/regressiontests/m2m_through_regress/models.py
@@ -62,18 +62,21 @@ class B(models.Model):
# Using to_field on the through model
@python_2_unicode_compatible
class Car(models.Model):
- make = models.CharField(max_length=20, unique=True)
+ make = models.CharField(max_length=20, unique=True, null=True)
drivers = models.ManyToManyField('Driver', through='CarDriver')
def __str__(self):
- return self.make
+ return "%s" % self.make
@python_2_unicode_compatible
class Driver(models.Model):
- name = models.CharField(max_length=20, unique=True)
+ name = models.CharField(max_length=20, unique=True, null=True)
def __str__(self):
- return self.name
+ return "%s" % self.name
+
+ class Meta:
+ ordering = ('name',)
@python_2_unicode_compatible
class CarDriver(models.Model):
diff --git a/tests/regressiontests/m2m_through_regress/tests.py b/tests/regressiontests/m2m_through_regress/tests.py
index 458c194f89..17a4525442 100644
--- a/tests/regressiontests/m2m_through_regress/tests.py
+++ b/tests/regressiontests/m2m_through_regress/tests.py
@@ -28,7 +28,8 @@ class M2MThroughTestCase(TestCase):
bob.group_set.all(), [
"<Group: Rock>",
"<Group: Roll>",
- ]
+ ],
+ ordered=False
)
self.assertQuerysetEqual(
@@ -51,7 +52,8 @@ class M2MThroughTestCase(TestCase):
frank.group_set.all(), [
"<Group: Rock>",
"<Group: Roll>",
- ]
+ ],
+ ordered=False
)
self.assertQuerysetEqual(
@@ -123,18 +125,105 @@ class ToFieldThroughTests(TestCase):
self.car = Car.objects.create(make="Toyota")
self.driver = Driver.objects.create(name="Ryan Briscoe")
CarDriver.objects.create(car=self.car, driver=self.driver)
+ # We are testing if wrong objects get deleted due to using wrong
+ # field value in m2m queries. So, it is essential that the pk
+ # numberings do not match.
+ # Create one intentionally unused driver to mix up the autonumbering
+ self.unused_driver = Driver.objects.create(name="Barney Gumble")
+ # And two intentionally unused cars.
+ self.unused_car1 = Car.objects.create(make="Trabant")
+ self.unused_car2 = Car.objects.create(make="Wartburg")
def test_to_field(self):
self.assertQuerysetEqual(
self.car.drivers.all(),
["<Driver: Ryan Briscoe>"]
- )
+ )
def test_to_field_reverse(self):
self.assertQuerysetEqual(
self.driver.car_set.all(),
["<Car: Toyota>"]
- )
+ )
+
+ def test_to_field_clear_reverse(self):
+ self.driver.car_set.clear()
+ self.assertQuerysetEqual(
+ self.driver.car_set.all(),[])
+
+ def test_to_field_clear(self):
+ self.car.drivers.clear()
+ self.assertQuerysetEqual(
+ self.car.drivers.all(),[])
+
+ # Low level tests for _add_items and _remove_items. We test these methods
+ # because .add/.remove aren't available for m2m fields with through, but
+ # through is the only way to set to_field currently. We do want to make
+ # sure these methods are ready if the ability to use .add or .remove with
+ # to_field relations is added some day.
+ def test_add(self):
+ self.assertQuerysetEqual(
+ self.car.drivers.all(),
+ ["<Driver: Ryan Briscoe>"]
+ )
+ # Yikes - barney is going to drive...
+ self.car.drivers._add_items('car', 'driver', self.unused_driver)
+ self.assertQuerysetEqual(
+ self.car.drivers.all(),
+ ["<Driver: Barney Gumble>", "<Driver: Ryan Briscoe>"]
+ )
+
+ def test_add_null(self):
+ nullcar = Car.objects.create(make=None)
+ with self.assertRaises(ValueError):
+ nullcar.drivers._add_items('car', 'driver', self.unused_driver)
+
+ def test_add_related_null(self):
+ nulldriver = Driver.objects.create(name=None)
+ with self.assertRaises(ValueError):
+ self.car.drivers._add_items('car', 'driver', nulldriver)
+
+ def test_add_reverse(self):
+ car2 = Car.objects.create(make="Honda")
+ self.assertQuerysetEqual(
+ self.driver.car_set.all(),
+ ["<Car: Toyota>"]
+ )
+ self.driver.car_set._add_items('driver', 'car', car2)
+ self.assertQuerysetEqual(
+ self.driver.car_set.all(),
+ ["<Car: Toyota>", "<Car: Honda>"],
+ ordered=False
+ )
+
+ def test_add_null_reverse(self):
+ nullcar = Car.objects.create(make=None)
+ with self.assertRaises(ValueError):
+ self.driver.car_set._add_items('driver', 'car', nullcar)
+
+ def test_add_null_reverse_related(self):
+ nulldriver = Driver.objects.create(name=None)
+ with self.assertRaises(ValueError):
+ nulldriver.car_set._add_items('driver', 'car', self.car)
+
+ def test_remove(self):
+ self.assertQuerysetEqual(
+ self.car.drivers.all(),
+ ["<Driver: Ryan Briscoe>"]
+ )
+ self.car.drivers._remove_items('car', 'driver', self.driver)
+ self.assertQuerysetEqual(
+ self.car.drivers.all(),[])
+
+ def test_remove_reverse(self):
+ self.assertQuerysetEqual(
+ self.driver.car_set.all(),
+ ["<Car: Toyota>"]
+ )
+ self.driver.car_set._remove_items('driver', 'car', self.car)
+ self.assertQuerysetEqual(
+ self.driver.car_set.all(),[])
+
class ThroughLoadDataTestCase(TestCase):
fixtures = ["m2m_through"]
diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py
index 33898cc1d5..b798cb21aa 100644
--- a/tests/regressiontests/mail/tests.py
+++ b/tests/regressiontests/mail/tests.py
@@ -17,6 +17,7 @@ from django.core.mail.backends import console, dummy, locmem, filebased, smtp
from django.core.mail.message import BadHeaderError
from django.test import TestCase
from django.test.utils import override_settings
+from django.utils.encoding import force_str, force_text
from django.utils.six import PY3, StringIO
from django.utils.translation import ugettext_lazy
@@ -357,6 +358,14 @@ class BaseEmailBackendTests(object):
self.assertEqual(message["from"], "from@example.com")
self.assertEqual(message.get_all("to"), ["to@example.com"])
+ def test_send_unicode(self):
+ email = EmailMessage('Chère maman', 'Je t\'aime très fort', 'from@example.com', ['to@example.com'])
+ num_sent = mail.get_connection().send_messages([email])
+ self.assertEqual(num_sent, 1)
+ message = self.get_the_message()
+ self.assertEqual(message["subject"], '=?utf-8?q?Ch=C3=A8re_maman?=')
+ self.assertEqual(force_text(message.get_payload()), 'Je t\'aime très fort')
+
def test_send_many(self):
email1 = EmailMessage('Subject', 'Content1', 'from@example.com', ['to@example.com'])
email2 = EmailMessage('Subject', 'Content2', 'from@example.com', ['to@example.com'])
@@ -526,8 +535,8 @@ class FileBackendTests(BaseEmailBackendTests, TestCase):
messages = []
for filename in os.listdir(self.tmp_dir):
with open(os.path.join(self.tmp_dir, filename), 'r') as fp:
- session = fp.read().split('\n' + ('-' * 79) + '\n')
- messages.extend(email.message_from_string(str(m)) for m in session if m)
+ session = force_text(fp.read()).split('\n' + ('-' * 79) + '\n')
+ messages.extend(email.message_from_string(force_str(m)) for m in session if m)
return messages
def test_file_sessions(self):
@@ -579,8 +588,8 @@ class ConsoleBackendTests(BaseEmailBackendTests, TestCase):
self.stream = sys.stdout = StringIO()
def get_mailbox_content(self):
- messages = self.stream.getvalue().split('\n' + ('-' * 79) + '\n')
- return [email.message_from_string(str(m)) for m in messages if m]
+ messages = force_text(self.stream.getvalue()).split('\n' + ('-' * 79) + '\n')
+ return [email.message_from_string(force_str(m)) for m in messages if m]
def test_console_stream_kwarg(self):
"""
diff --git a/tests/regressiontests/managers_regress/tests.py b/tests/regressiontests/managers_regress/tests.py
index f3721a4c01..45059be4e5 100644
--- a/tests/regressiontests/managers_regress/tests.py
+++ b/tests/regressiontests/managers_regress/tests.py
@@ -61,7 +61,8 @@ class ManagersRegressionTests(TestCase):
self.assertQuerysetEqual(Child4.manager1.all(), [
"<Child4: d1>",
"<Child4: f1>"
- ]
+ ],
+ ordered=False
)
self.assertQuerysetEqual(Child5._default_manager.all(), ["<Child5: fred>"])
self.assertQuerysetEqual(Child6._default_manager.all(), ["<Child6: f1>"])
diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py
index de901f4a80..b8cffd9c92 100644
--- a/tests/regressiontests/middleware/tests.py
+++ b/tests/regressiontests/middleware/tests.py
@@ -294,6 +294,15 @@ class CommonMiddlewareTest(TestCase):
CommonMiddleware().process_response(request, response)
self.assertEqual(len(mail.outbox), 0)
+ # Other tests
+
+ def test_non_ascii_query_string_does_not_crash(self):
+ """Regression test for #15152"""
+ request = self._get_request('slash')
+ request.META['QUERY_STRING'] = 'drink=café'
+ response = CommonMiddleware().process_request(request)
+ self.assertEqual(response.status_code, 301)
+
class ConditionalGetMiddlewareTest(TestCase):
urls = 'regressiontests.middleware.cond_get_urls'
diff --git a/tests/regressiontests/model_fields/imagefield.py b/tests/regressiontests/model_fields/imagefield.py
index 7446f222ff..df0215db3d 100644
--- a/tests/regressiontests/model_fields/imagefield.py
+++ b/tests/regressiontests/model_fields/imagefield.py
@@ -6,6 +6,7 @@ import shutil
from django.core.files import File
from django.core.files.images import ImageFile
from django.test import TestCase
+from django.utils._os import upath
from django.utils.unittest import skipIf
from .models import Image
@@ -43,10 +44,10 @@ class ImageFieldTestMixin(object):
shutil.rmtree(temp_storage_dir)
os.mkdir(temp_storage_dir)
- file_path1 = os.path.join(os.path.dirname(__file__), "4x8.png")
+ file_path1 = os.path.join(os.path.dirname(upath(__file__)), "4x8.png")
self.file1 = self.File(open(file_path1, 'rb'))
- file_path2 = os.path.join(os.path.dirname(__file__), "8x4.png")
+ file_path2 = os.path.join(os.path.dirname(upath(__file__)), "8x4.png")
self.file2 = self.File(open(file_path2, 'rb'))
def tearDown(self):
diff --git a/tests/regressiontests/model_forms_regress/models.py b/tests/regressiontests/model_forms_regress/models.py
index f6e08d24dc..2c2fd39158 100644
--- a/tests/regressiontests/model_forms_regress/models.py
+++ b/tests/regressiontests/model_forms_regress/models.py
@@ -5,6 +5,7 @@ import os
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
+from django.utils._os import upath
class Person(models.Model):
@@ -19,7 +20,7 @@ class Triple(models.Model):
unique_together = (('left', 'middle'), ('middle', 'right'))
class FilePathModel(models.Model):
- path = models.FilePathField(path=os.path.dirname(__file__), match=".*\.py$", blank=True)
+ path = models.FilePathField(path=os.path.dirname(upath(__file__)), match=".*\.py$", blank=True)
@python_2_unicode_compatible
class Publication(models.Model):
diff --git a/tests/regressiontests/model_formsets_regress/tests.py b/tests/regressiontests/model_formsets_regress/tests.py
index 1fbdb9744f..8cadcfc409 100644
--- a/tests/regressiontests/model_formsets_regress/tests.py
+++ b/tests/regressiontests/model_formsets_regress/tests.py
@@ -351,7 +351,7 @@ class FormfieldShouldDeleteFormTests(TestCase):
def should_delete(self):
""" delete form if odd PK """
- return self.instance.id % 2 != 0
+ return self.instance.pk % 2 != 0
NormalFormset = modelformset_factory(User, form=CustomDeleteUserForm, can_delete=True)
DeleteFormset = modelformset_factory(User, form=CustomDeleteUserForm, formset=BaseCustomDeleteModelFormSet)
@@ -392,7 +392,7 @@ class FormfieldShouldDeleteFormTests(TestCase):
data = dict(self.data)
data['form-INITIAL_FORMS'] = 4
data.update(dict(
- ('form-%d-id' % i, user.id)
+ ('form-%d-id' % i, user.pk)
for i,user in enumerate(User.objects.all())
))
formset = self.NormalFormset(data, queryset=User.objects.all())
@@ -409,7 +409,7 @@ class FormfieldShouldDeleteFormTests(TestCase):
data = dict(self.data)
data['form-INITIAL_FORMS'] = 4
data.update(dict(
- ('form-%d-id' % i, user.id)
+ ('form-%d-id' % i, user.pk)
for i,user in enumerate(User.objects.all())
))
data.update(self.delete_all_ids)
@@ -428,7 +428,7 @@ class FormfieldShouldDeleteFormTests(TestCase):
data = dict(self.data)
data['form-INITIAL_FORMS'] = 4
data.update(dict(
- ('form-%d-id' % i, user.id)
+ ('form-%d-id' % i, user.pk)
for i,user in enumerate(User.objects.all())
))
data.update(self.delete_all_ids)
@@ -440,5 +440,5 @@ class FormfieldShouldDeleteFormTests(TestCase):
self.assertEqual(len(User.objects.all()), 2)
# verify no "odd" PKs left
- odd_ids = [user.id for user in User.objects.all() if user.id % 2]
+ odd_ids = [user.pk for user in User.objects.all() if user.pk % 2]
self.assertEqual(len(odd_ids), 0)
diff --git a/tests/regressiontests/model_regress/tests.py b/tests/regressiontests/model_regress/tests.py
index 6a45a83052..c90fe98658 100644
--- a/tests/regressiontests/model_regress/tests.py
+++ b/tests/regressiontests/model_regress/tests.py
@@ -71,7 +71,8 @@ class ModelTests(TestCase):
datetime.date(1999, 12, 31),
datetime.date(1998, 12, 31),
],
- attrgetter("when")
+ attrgetter("when"),
+ ordered=False
)
self.assertQuerysetEqual(
Party.objects.filter(when__year=1998), [
@@ -85,14 +86,16 @@ class ModelTests(TestCase):
datetime.date(1999, 12, 31),
datetime.date(1998, 12, 31),
],
- attrgetter("when")
+ attrgetter("when"),
+ ordered=False
)
self.assertQuerysetEqual(
Party.objects.filter(when__month="12"), [
datetime.date(1999, 12, 31),
datetime.date(1998, 12, 31),
],
- attrgetter("when")
+ attrgetter("when"),
+ ordered=False
)
self.assertQuerysetEqual(
Party.objects.filter(when__year="1998"), [
diff --git a/tests/regressiontests/multiple_database/tests.py b/tests/regressiontests/multiple_database/tests.py
index 79fe6beebd..a903ea11a0 100644
--- a/tests/regressiontests/multiple_database/tests.py
+++ b/tests/regressiontests/multiple_database/tests.py
@@ -16,16 +16,6 @@ from django.utils.six import StringIO
from .models import Book, Person, Pet, Review, UserProfile
-def copy_content_types_from_default_to_other():
- # On post_syncdb, content types are created in the 'default' database.
- # However, tests of generic foreign keys require them in 'other' too.
- # The problem is masked on backends that defer constraints checks: at the
- # end of each test, there's a rollback, and constraints are never checked.
- # It only appears on MySQL + InnoDB.
- for ct in ContentType.objects.using('default').all():
- ct.save(using='other')
-
-
class QueryTestCase(TestCase):
multi_db = True
@@ -705,8 +695,6 @@ class QueryTestCase(TestCase):
def test_generic_key_separation(self):
"Generic fields are constrained to a single database"
- copy_content_types_from_default_to_other()
-
# Create a book and author on the default database
pro = Book.objects.create(title="Pro Django",
published=datetime.date(2008, 12, 16))
@@ -734,8 +722,6 @@ class QueryTestCase(TestCase):
def test_generic_key_reverse_operations(self):
"Generic reverse manipulations are all constrained to a single DB"
- copy_content_types_from_default_to_other()
-
dive = Book.objects.using('other').create(title="Dive into Python",
published=datetime.date(2009, 5, 4))
@@ -780,8 +766,6 @@ class QueryTestCase(TestCase):
def test_generic_key_cross_database_protection(self):
"Operations that involve sharing generic key objects across databases raise an error"
- copy_content_types_from_default_to_other()
-
# Create a book and author on the default database
pro = Book.objects.create(title="Pro Django",
published=datetime.date(2008, 12, 16))
@@ -833,8 +817,6 @@ class QueryTestCase(TestCase):
def test_generic_key_deletion(self):
"Cascaded deletions of Generic Key relations issue queries on the right database"
- copy_content_types_from_default_to_other()
-
dive = Book.objects.using('other').create(title="Dive into Python",
published=datetime.date(2009, 5, 4))
review = Review.objects.using('other').create(source="Python Weekly", content_object=dive)
@@ -1402,8 +1384,6 @@ class RouterTestCase(TestCase):
def test_generic_key_cross_database_protection(self):
"Generic Key operations can span databases if they share a source"
- copy_content_types_from_default_to_other()
-
# Create a book and author on the default database
pro = Book.objects.using('default'
).create(title="Pro Django", published=datetime.date(2008, 12, 16))
@@ -1515,8 +1495,6 @@ class RouterTestCase(TestCase):
def test_generic_key_managers(self):
"Generic key relations are represented by managers, and can be controlled like managers"
- copy_content_types_from_default_to_other()
-
pro = Book.objects.using('other').create(title="Pro Django",
published=datetime.date(2008, 12, 16))
@@ -1922,3 +1900,39 @@ class RouterModelArgumentTestCase(TestCase):
pet = Pet.objects.create(owner=person, name='Wart')
# test related FK collection
person.delete()
+
+
+class SyncOnlyDefaultDatabaseRouter(object):
+ def allow_syncdb(self, db, model):
+ return db == DEFAULT_DB_ALIAS
+
+
+class SyncDBTestCase(TestCase):
+ multi_db = True
+
+ def test_syncdb_to_other_database(self):
+ """Regression test for #16039: syncdb with --database option."""
+ cts = ContentType.objects.using('other').filter(app_label='multiple_database')
+
+ count = cts.count()
+ self.assertGreater(count, 0)
+
+ cts.delete()
+ management.call_command('syncdb', verbosity=0, interactive=False,
+ load_initial_data=False, database='other')
+ self.assertEqual(cts.count(), count)
+
+ def test_syncdb_to_other_database_with_router(self):
+ """Regression test for #16039: syncdb with --database option."""
+ cts = ContentType.objects.using('other').filter(app_label='multiple_database')
+
+ cts.delete()
+ try:
+ old_routers = router.routers
+ router.routers = [SyncOnlyDefaultDatabaseRouter()]
+ management.call_command('syncdb', verbosity=0, interactive=False,
+ load_initial_data=False, database='other')
+ finally:
+ router.routers = old_routers
+
+ self.assertEqual(cts.count(), 0)
diff --git a/tests/regressiontests/pagination/__init__.py b/tests/regressiontests/pagination/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/pagination/__init__.py
diff --git a/tests/regressiontests/pagination/custom.py b/tests/regressiontests/pagination/custom.py
new file mode 100644
index 0000000000..47a932c7e3
--- /dev/null
+++ b/tests/regressiontests/pagination/custom.py
@@ -0,0 +1,20 @@
+from django.core.paginator import Paginator, Page
+
+
+class ValidAdjacentNumsPage(Page):
+
+ def next_page_number(self):
+ if not self.has_next():
+ return None
+ return super(ValidAdjacentNumsPage, self).next_page_number()
+
+ def previous_page_number(self):
+ if not self.has_previous():
+ return None
+ return super(ValidAdjacentNumsPage, self).previous_page_number()
+
+
+class ValidAdjacentNumsPaginator(Paginator):
+
+ def _get_page(self, *args, **kwargs):
+ return ValidAdjacentNumsPage(*args, **kwargs)
diff --git a/tests/modeltests/pagination/models.py b/tests/regressiontests/pagination/models.py
index 779d3029ba..9dc8d4b776 100644
--- a/tests/modeltests/pagination/models.py
+++ b/tests/regressiontests/pagination/models.py
@@ -1,11 +1,3 @@
-"""
-30. Object pagination
-
-Django provides a framework for paginating a list of objects in a few lines
-of code. This is often useful for dividing search results or long lists of
-objects into easily readable pages.
-"""
-
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
diff --git a/tests/regressiontests/pagination_regress/tests.py b/tests/regressiontests/pagination/tests.py
index e98352e006..dea5756672 100644
--- a/tests/regressiontests/pagination_regress/tests.py
+++ b/tests/regressiontests/pagination/tests.py
@@ -1,9 +1,18 @@
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
-from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
-from django.utils.unittest import TestCase
+from datetime import datetime
-class PaginatorTests(TestCase):
+from django.core.paginator import (Paginator, EmptyPage, InvalidPage,
+ PageNotAnInteger)
+from django.test import TestCase
+from django.utils import six
+from django.utils import unittest
+
+from .models import Article
+from .custom import ValidAdjacentNumsPaginator
+
+
+class PaginationTests(unittest.TestCase):
"""
Tests for the Paginator and Page classes.
"""
@@ -31,15 +40,6 @@ class PaginatorTests(TestCase):
"For '%s', expected %s but got %s. Paginator parameters were: %s"
% (name, expected, got, params))
- def test_invalid_page_number(self):
- """
- Tests that invalid page numbers result in the correct exception being
- raised.
- """
- paginator = Paginator([1, 2, 3], 2)
- self.assertRaises(PageNotAnInteger, paginator.validate_number, None)
- self.assertRaises(PageNotAnInteger, paginator.validate_number, 'x')
-
def test_paginator(self):
"""
Tests the paginator attributes using varying inputs.
@@ -107,6 +107,38 @@ class PaginatorTests(TestCase):
for params, output in tests:
self.check_paginator(params, output)
+ def test_invalid_page_number(self):
+ """
+ Tests that invalid page numbers result in the correct exception being
+ raised.
+ """
+ paginator = Paginator([1, 2, 3], 2)
+ self.assertRaises(InvalidPage, paginator.page, 3)
+ self.assertRaises(PageNotAnInteger, paginator.validate_number, None)
+ self.assertRaises(PageNotAnInteger, paginator.validate_number, 'x')
+ # With no content and allow_empty_first_page=True, 1 is a valid page number
+ paginator = Paginator([], 2)
+ self.assertEqual(paginator.validate_number(1), 1)
+
+ def test_paginate_misc_classes(self):
+ class CountContainer(object):
+ def count(self):
+ return 42
+ # Paginator can be passed other objects with a count() method.
+ paginator = Paginator(CountContainer(), 10)
+ self.assertEqual(42, paginator.count)
+ self.assertEqual(5, paginator.num_pages)
+ self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range))
+
+ # Paginator can be passed other objects that implement __len__.
+ class LenContainer(object):
+ def __len__(self):
+ return 42
+ paginator = Paginator(LenContainer(), 10)
+ self.assertEqual(42, paginator.count)
+ self.assertEqual(5, paginator.num_pages)
+ self.assertEqual([1, 2, 3, 4, 5], list(paginator.page_range))
+
def check_indexes(self, params, page_num, indexes):
"""
Helper method that instantiates a Paginator object from the passed
@@ -168,6 +200,7 @@ class PaginatorTests(TestCase):
for params, first, last in tests:
self.check_indexes(params, 'first', first)
self.check_indexes(params, 'last', last)
+
# When no items and no empty first page, we should get EmptyPage error.
self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 0, False), 1, None)
self.assertRaises(EmptyPage, self.check_indexes, ([], 4, 1, False), 1, None)
@@ -184,3 +217,91 @@ class PaginatorTests(TestCase):
self.assertFalse('a' in page2)
self.assertEqual(''.join(page2), 'fghijk')
self.assertEqual(''.join(reversed(page2)), 'kjihgf')
+
+ def test_get_page_hook(self):
+ """
+ Tests that a Paginator subclass can use the ``_get_page`` hook to
+ return an alternative to the standard Page class.
+ """
+ eleven = 'abcdefghijk'
+ paginator = ValidAdjacentNumsPaginator(eleven, per_page=6)
+ page1 = paginator.page(1)
+ page2 = paginator.page(2)
+ self.assertIsNone(page1.previous_page_number())
+ self.assertEqual(page1.next_page_number(), 2)
+ self.assertEqual(page2.previous_page_number(), 1)
+ self.assertIsNone(page2.next_page_number())
+
+
+class ModelPaginationTests(TestCase):
+ """
+ Test pagination with Django model instances
+ """
+ def setUp(self):
+ # Prepare a list of objects for pagination.
+ for x in range(1, 10):
+ a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29))
+ a.save()
+
+ def test_first_page(self):
+ paginator = Paginator(Article.objects.all(), 5)
+ p = paginator.page(1)
+ self.assertEqual("<Page 1 of 2>", six.text_type(p))
+ self.assertQuerysetEqual(p.object_list, [
+ "<Article: Article 1>",
+ "<Article: Article 2>",
+ "<Article: Article 3>",
+ "<Article: Article 4>",
+ "<Article: Article 5>"
+ ],
+ ordered=False
+ )
+ self.assertTrue(p.has_next())
+ self.assertFalse(p.has_previous())
+ self.assertTrue(p.has_other_pages())
+ self.assertEqual(2, p.next_page_number())
+ self.assertRaises(InvalidPage, p.previous_page_number)
+ self.assertEqual(1, p.start_index())
+ self.assertEqual(5, p.end_index())
+
+ def test_last_page(self):
+ paginator = Paginator(Article.objects.all(), 5)
+ p = paginator.page(2)
+ self.assertEqual("<Page 2 of 2>", six.text_type(p))
+ self.assertQuerysetEqual(p.object_list, [
+ "<Article: Article 6>",
+ "<Article: Article 7>",
+ "<Article: Article 8>",
+ "<Article: Article 9>"
+ ],
+ ordered=False
+ )
+ self.assertFalse(p.has_next())
+ self.assertTrue(p.has_previous())
+ self.assertTrue(p.has_other_pages())
+ self.assertRaises(InvalidPage, p.next_page_number)
+ self.assertEqual(1, p.previous_page_number())
+ self.assertEqual(6, p.start_index())
+ self.assertEqual(9, p.end_index())
+
+ def test_page_getitem(self):
+ """
+ Tests proper behaviour of a paginator page __getitem__ (queryset
+ evaluation, slicing, exception raised).
+ """
+ paginator = Paginator(Article.objects.all(), 5)
+ p = paginator.page(1)
+
+ # Make sure object_list queryset is not evaluated by an invalid __getitem__ call.
+ # (this happens from the template engine when using eg: {% page_obj.has_previous %})
+ self.assertIsNone(p.object_list._result_cache)
+ self.assertRaises(TypeError, lambda: p['has_previous'])
+ self.assertIsNone(p.object_list._result_cache)
+
+ # Make sure slicing the Page object with numbers and slice objects work.
+ self.assertEqual(p[0], Article.objects.get(headline='Article 1'))
+ self.assertQuerysetEqual(p[slice(2)], [
+ "<Article: Article 1>",
+ "<Article: Article 2>",
+ ]
+ )
diff --git a/tests/regressiontests/pagination_regress/models.py b/tests/regressiontests/pagination_regress/models.py
deleted file mode 100644
index cde172db68..0000000000
--- a/tests/regressiontests/pagination_regress/models.py
+++ /dev/null
@@ -1 +0,0 @@
-# Models file for tests to run.
diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py
index f0178a0256..73b9762150 100644
--- a/tests/regressiontests/queries/models.py
+++ b/tests/regressiontests/queries/models.py
@@ -283,6 +283,7 @@ class SingleObject(models.Model):
class RelatedObject(models.Model):
single = models.ForeignKey(SingleObject, null=True)
+ f = models.IntegerField(null=True)
class Meta:
ordering = ['single']
@@ -311,7 +312,7 @@ class Food(models.Model):
@python_2_unicode_compatible
class Eaten(models.Model):
- food = models.ForeignKey(Food, to_field="name")
+ food = models.ForeignKey(Food, to_field="name", null=True)
meal = models.CharField(max_length=20)
def __str__(self):
@@ -400,3 +401,23 @@ class ModelA(models.Model):
name = models.TextField()
b = models.ForeignKey(ModelB, null=True)
d = models.ForeignKey(ModelD)
+
+@python_2_unicode_compatible
+class Job(models.Model):
+ name = models.CharField(max_length=20, unique=True)
+
+ def __str__(self):
+ return self.name
+
+class JobResponsibilities(models.Model):
+ job = models.ForeignKey(Job, to_field='name')
+ responsibility = models.ForeignKey('Responsibility', to_field='description')
+
+@python_2_unicode_compatible
+class Responsibility(models.Model):
+ description = models.CharField(max_length=20, unique=True)
+ jobs = models.ManyToManyField(Job, through=JobResponsibilities,
+ related_name='responsibilities')
+
+ def __str__(self):
+ return self.description
diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py
index c2a4ad1caf..75e27769b4 100644
--- a/tests/regressiontests/queries/tests.py
+++ b/tests/regressiontests/queries/tests.py
@@ -23,7 +23,8 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover,
Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten,
Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory,
SpecialCategory, OneToOneCategory, NullableName, ProxyCategory,
- SingleObject, RelatedObject, ModelA, ModelD)
+ SingleObject, RelatedObject, ModelA, ModelD, Responsibility, Job,
+ JobResponsibilities)
class BaseQuerysetTest(TestCase):
@@ -243,7 +244,10 @@ class Queries1Tests(BaseQuerysetTest):
q1 = Item.objects.order_by('name')
q2 = Item.objects.filter(id=self.i1.id)
list(q2)
- self.assertEqual(len((q1 & q2).order_by('name').query.tables), 1)
+ combined_query = (q1 & q2).order_by('name').query
+ self.assertEqual(len([
+ t for t in combined_query.tables if combined_query.alias_refcount[t]
+ ]), 1)
def test_order_by_join_unref(self):
"""
@@ -841,11 +845,14 @@ class Queries1Tests(BaseQuerysetTest):
"""
original_ordering = Tag._meta.ordering
Tag._meta.ordering = None
- self.assertQuerysetEqual(
- Tag.objects.all(),
- ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'],
- )
- Tag._meta.ordering = original_ordering
+ try:
+ self.assertQuerysetEqual(
+ Tag.objects.all(),
+ ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'],
+ ordered=False
+ )
+ finally:
+ Tag._meta.ordering = original_ordering
def test_exclude(self):
self.assertQuerysetEqual(
@@ -880,6 +887,225 @@ class Queries1Tests(BaseQuerysetTest):
Item.objects.filter(Q(tags__name__in=['t4', 't3'])),
[repr(i) for i in Item.objects.filter(~~Q(tags__name__in=['t4', 't3']))])
+ def test_ticket_10790_1(self):
+ # Querying direct fields with isnull should trim the left outer join.
+ # It also should not create INNER JOIN.
+ q = Tag.objects.filter(parent__isnull=True)
+
+ self.assertQuerysetEqual(q, ['<Tag: t1>'])
+ self.assertTrue('JOIN' not in str(q.query))
+
+ q = Tag.objects.filter(parent__isnull=False)
+
+ self.assertQuerysetEqual(
+ q,
+ ['<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'],
+ )
+ self.assertTrue('JOIN' not in str(q.query))
+
+ q = Tag.objects.exclude(parent__isnull=True)
+ self.assertQuerysetEqual(
+ q,
+ ['<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'],
+ )
+ self.assertTrue('JOIN' not in str(q.query))
+
+ q = Tag.objects.exclude(parent__isnull=False)
+ self.assertQuerysetEqual(q, ['<Tag: t1>'])
+ self.assertTrue('JOIN' not in str(q.query))
+
+ q = Tag.objects.exclude(parent__parent__isnull=False)
+
+ self.assertQuerysetEqual(
+ q,
+ ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>'],
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 1)
+ self.assertTrue('INNER JOIN' not in str(q.query))
+
+ def test_ticket_10790_2(self):
+ # Querying across several tables should strip only the last outer join,
+ # while preserving the preceeding inner joins.
+ q = Tag.objects.filter(parent__parent__isnull=False)
+
+ self.assertQuerysetEqual(
+ q,
+ ['<Tag: t4>', '<Tag: t5>'],
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 1)
+
+ # Querying without isnull should not convert anything to left outer join.
+ q = Tag.objects.filter(parent__parent=self.t1)
+ self.assertQuerysetEqual(
+ q,
+ ['<Tag: t4>', '<Tag: t5>'],
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 1)
+
+ def test_ticket_10790_3(self):
+ # Querying via indirect fields should populate the left outer join
+ q = NamedCategory.objects.filter(tag__isnull=True)
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 1)
+ # join to dumbcategory ptr_id
+ self.assertTrue(str(q.query).count('INNER JOIN') == 1)
+ self.assertQuerysetEqual(q, [])
+
+ # Querying across several tables should strip only the last join, while
+ # preserving the preceding left outer joins.
+ q = NamedCategory.objects.filter(tag__parent__isnull=True)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 1)
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 1)
+ self.assertQuerysetEqual( q, ['<NamedCategory: NamedCategory object>'])
+
+ def test_ticket_10790_4(self):
+ # Querying across m2m field should not strip the m2m table from join.
+ q = Author.objects.filter(item__tags__isnull=True)
+ self.assertQuerysetEqual(
+ q,
+ ['<Author: a2>', '<Author: a3>'],
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 2)
+ self.assertTrue('INNER JOIN' not in str(q.query))
+
+ q = Author.objects.filter(item__tags__parent__isnull=True)
+ self.assertQuerysetEqual(
+ q,
+ ['<Author: a1>', '<Author: a2>', '<Author: a2>', '<Author: a3>'],
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 3)
+ self.assertTrue('INNER JOIN' not in str(q.query))
+
+ def test_ticket_10790_5(self):
+ # Querying with isnull=False across m2m field should not create outer joins
+ q = Author.objects.filter(item__tags__isnull=False)
+ self.assertQuerysetEqual(
+ q,
+ ['<Author: a1>', '<Author: a1>', '<Author: a2>', '<Author: a2>', '<Author: a4>']
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 2)
+
+ q = Author.objects.filter(item__tags__parent__isnull=False)
+ self.assertQuerysetEqual(
+ q,
+ ['<Author: a1>', '<Author: a2>', '<Author: a4>']
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 3)
+
+ q = Author.objects.filter(item__tags__parent__parent__isnull=False)
+ self.assertQuerysetEqual(
+ q,
+ ['<Author: a4>']
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 4)
+
+ def test_ticket_10790_6(self):
+ # Querying with isnull=True across m2m field should not create inner joins
+ # and strip last outer join
+ q = Author.objects.filter(item__tags__parent__parent__isnull=True)
+ self.assertQuerysetEqual(
+ q,
+ ['<Author: a1>', '<Author: a1>', '<Author: a2>', '<Author: a2>',
+ '<Author: a2>', '<Author: a3>']
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 4)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 0)
+
+ q = Author.objects.filter(item__tags__parent__isnull=True)
+ self.assertQuerysetEqual(
+ q,
+ ['<Author: a1>', '<Author: a2>', '<Author: a2>', '<Author: a3>']
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 3)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 0)
+
+ def test_ticket_10790_7(self):
+ # Reverse querying with isnull should not strip the join
+ q = Author.objects.filter(item__isnull=True)
+ self.assertQuerysetEqual(
+ q,
+ ['<Author: a3>']
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 1)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 0)
+
+ q = Author.objects.filter(item__isnull=False)
+ self.assertQuerysetEqual(
+ q,
+ ['<Author: a1>', '<Author: a2>', '<Author: a2>', '<Author: a4>']
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 1)
+
+ def test_ticket_10790_8(self):
+ # Querying with combined q-objects should also strip the left outer join
+ q = Tag.objects.filter(Q(parent__isnull=True) | Q(parent=self.t1))
+ self.assertQuerysetEqual(
+ q,
+ ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>']
+ )
+ self.assertTrue(str(q.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q.query).count('INNER JOIN') == 0)
+
+ def test_ticket_10790_combine(self):
+ # Combining queries should not re-populate the left outer join
+ q1 = Tag.objects.filter(parent__isnull=True)
+ q2 = Tag.objects.filter(parent__isnull=False)
+
+ q3 = q1 | q2
+ self.assertQuerysetEqual(
+ q3,
+ ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>', '<Tag: t4>', '<Tag: t5>'],
+ )
+ self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q3.query).count('INNER JOIN') == 0)
+
+ q3 = q1 & q2
+ self.assertQuerysetEqual(q3, [])
+ self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q3.query).count('INNER JOIN') == 0)
+
+ q2 = Tag.objects.filter(parent=self.t1)
+ q3 = q1 | q2
+ self.assertQuerysetEqual(
+ q3,
+ ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>']
+ )
+ self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q3.query).count('INNER JOIN') == 0)
+
+ q3 = q2 | q1
+ self.assertQuerysetEqual(
+ q3,
+ ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>']
+ )
+ self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 0)
+ self.assertTrue(str(q3.query).count('INNER JOIN') == 0)
+
+ q1 = Tag.objects.filter(parent__isnull=True)
+ q2 = Tag.objects.filter(parent__parent__isnull=True)
+
+ q3 = q1 | q2
+ self.assertQuerysetEqual(
+ q3,
+ ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>']
+ )
+ self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 1)
+ self.assertTrue(str(q3.query).count('INNER JOIN') == 0)
+
+ q3 = q2 | q1
+ self.assertQuerysetEqual(
+ q3,
+ ['<Tag: t1>', '<Tag: t2>', '<Tag: t3>']
+ )
+ self.assertTrue(str(q3.query).count('LEFT OUTER JOIN') == 1)
+ self.assertTrue(str(q3.query).count('INNER JOIN') == 0)
+
+
class Queries2Tests(TestCase):
def setUp(self):
Number.objects.create(num=4)
@@ -925,15 +1151,18 @@ class Queries2Tests(TestCase):
self.assertQuerysetEqual(Number.objects.filter(num__gt=12.1), [])
self.assertQuerysetEqual(
Number.objects.filter(num__lt=12),
- ['<Number: 4>', '<Number: 8>']
+ ['<Number: 4>', '<Number: 8>'],
+ ordered=False
)
self.assertQuerysetEqual(
Number.objects.filter(num__lt=12.0),
- ['<Number: 4>', '<Number: 8>']
+ ['<Number: 4>', '<Number: 8>'],
+ ordered=False
)
self.assertQuerysetEqual(
Number.objects.filter(num__lt=12.1),
- ['<Number: 4>', '<Number: 8>', '<Number: 12>']
+ ['<Number: 4>', '<Number: 8>', '<Number: 12>'],
+ ordered=False
)
self.assertQuerysetEqual(
Number.objects.filter(num__gte=11.9),
@@ -951,23 +1180,28 @@ class Queries2Tests(TestCase):
self.assertQuerysetEqual(Number.objects.filter(num__gte=12.9), [])
self.assertQuerysetEqual(
Number.objects.filter(num__lte=11.9),
- ['<Number: 4>', '<Number: 8>']
+ ['<Number: 4>', '<Number: 8>'],
+ ordered=False
)
self.assertQuerysetEqual(
Number.objects.filter(num__lte=12),
- ['<Number: 4>', '<Number: 8>', '<Number: 12>']
+ ['<Number: 4>', '<Number: 8>', '<Number: 12>'],
+ ordered=False
)
self.assertQuerysetEqual(
Number.objects.filter(num__lte=12.0),
- ['<Number: 4>', '<Number: 8>', '<Number: 12>']
+ ['<Number: 4>', '<Number: 8>', '<Number: 12>'],
+ ordered=False
)
self.assertQuerysetEqual(
Number.objects.filter(num__lte=12.1),
- ['<Number: 4>', '<Number: 8>', '<Number: 12>']
+ ['<Number: 4>', '<Number: 8>', '<Number: 12>'],
+ ordered=False
)
self.assertQuerysetEqual(
Number.objects.filter(num__lte=12.9),
- ['<Number: 4>', '<Number: 8>', '<Number: 12>']
+ ['<Number: 4>', '<Number: 8>', '<Number: 12>'],
+ ordered=False
)
def test_ticket7411(self):
@@ -1026,14 +1260,18 @@ class Queries4Tests(BaseQuerysetTest):
Item.objects.create(name='i2', created=datetime.datetime.now(), note=n1, creator=self.a3)
def test_ticket14876(self):
+ # Note: when combining the query we need to have information available
+ # about the join type of the trimmed "creator__isnull" join. If we
+ # don't have that information, then the join is created as INNER JOIN
+ # and results will be incorrect.
q1 = Report.objects.filter(Q(creator__isnull=True) | Q(creator__extra__info='e1'))
q2 = Report.objects.filter(Q(creator__isnull=True)) | Report.objects.filter(Q(creator__extra__info='e1'))
- self.assertQuerysetEqual(q1, ["<Report: r1>", "<Report: r3>"])
+ self.assertQuerysetEqual(q1, ["<Report: r1>", "<Report: r3>"], ordered=False)
self.assertEqual(str(q1.query), str(q2.query))
q1 = Report.objects.filter(Q(creator__extra__info='e1') | Q(creator__isnull=True))
q2 = Report.objects.filter(Q(creator__extra__info='e1')) | Report.objects.filter(Q(creator__isnull=True))
- self.assertQuerysetEqual(q1, ["<Report: r1>", "<Report: r3>"])
+ self.assertQuerysetEqual(q1, ["<Report: r1>", "<Report: r3>"], ordered=False)
self.assertEqual(str(q1.query), str(q2.query))
q1 = Item.objects.filter(Q(creator=self.a1) | Q(creator__report__name='r1')).order_by()
@@ -1046,6 +1284,18 @@ class Queries4Tests(BaseQuerysetTest):
self.assertQuerysetEqual(q1, ["<Item: i1>"])
self.assertEqual(str(q1.query), str(q2.query))
+ def test_combine_join_reuse(self):
+ # Test that we correctly recreate joins having identical connections
+ # in the rhs query, in case the query is ORed together. Related to
+ # ticket #18748
+ Report.objects.create(name='r4', creator=self.a1)
+ q1 = Author.objects.filter(report__name='r5')
+ q2 = Author.objects.filter(report__name='r4').filter(report__name='r1')
+ combined = q1|q2
+ self.assertEquals(str(combined.query).count('JOIN'), 2)
+ self.assertEquals(len(combined), 1)
+ self.assertEquals(combined[0].name, 'a1')
+
def test_ticket7095(self):
# Updates that are filtered on the model being updated are somewhat
# tricky in MySQL. This exercises that case.
@@ -1382,17 +1632,19 @@ class NullableRelOrderingTests(TestCase):
# the join type of already existing joins.
Plaything.objects.create(name="p1")
s = SingleObject.objects.create(name='s')
- r = RelatedObject.objects.create(single=s)
+ r = RelatedObject.objects.create(single=s, f=1)
Plaything.objects.create(name="p2", others=r)
qs = Plaything.objects.all().filter(others__isnull=False).order_by('pk')
+ self.assertTrue('JOIN' not in str(qs.query))
+ qs = Plaything.objects.all().filter(others__f__isnull=False).order_by('pk')
self.assertTrue('INNER' in str(qs.query))
qs = qs.order_by('others__single__name')
# The ordering by others__single__pk will add one new join (to single)
# and that join must be LEFT join. The already existing join to related
# objects must be kept INNER. So, we have both a INNER and a LEFT join
# in the query.
- self.assertTrue('LEFT' in str(qs.query))
- self.assertTrue('INNER' in str(qs.query))
+ self.assertEquals(str(qs.query).count('LEFT'), 1)
+ self.assertEquals(str(qs.query).count('INNER'), 1)
self.assertQuerysetEqual(
qs,
['<Plaything: p2>']
@@ -1443,6 +1695,7 @@ class Queries6Tests(TestCase):
# 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,
@@ -1970,6 +2223,29 @@ class DefaultValuesInsertTest(TestCase):
except TypeError:
self.fail("Creation of an instance of a model with only the PK field shouldn't error out after bulk insert refactoring (#17056)")
+class ExcludeTest(TestCase):
+ def setUp(self):
+ f1 = Food.objects.create(name='apples')
+ Food.objects.create(name='oranges')
+ Eaten.objects.create(food=f1, meal='dinner')
+ j1 = Job.objects.create(name='Manager')
+ r1 = Responsibility.objects.create(description='Playing golf')
+ j2 = Job.objects.create(name='Programmer')
+ r2 = Responsibility.objects.create(description='Programming')
+ JobResponsibilities.objects.create(job=j1, responsibility=r1)
+ JobResponsibilities.objects.create(job=j2, responsibility=r2)
+
+ def test_to_field(self):
+ self.assertQuerysetEqual(
+ Food.objects.exclude(eaten__meal='dinner'),
+ ['<Food: oranges>'])
+ self.assertQuerysetEqual(
+ Job.objects.exclude(responsibilities__description='Playing golf'),
+ ['<Job: Programmer>'])
+ self.assertQuerysetEqual(
+ Responsibility.objects.exclude(jobs__name='Manager'),
+ ['<Responsibility: Programming>'])
+
class NullInExcludeTest(TestCase):
def setUp(self):
NullableName.objects.create(name='i1')
@@ -2132,3 +2408,13 @@ class NullJoinPromotionOrTest(TestCase):
# so we can use INNER JOIN for it. However, we can NOT use INNER JOIN
# for the b->c join, as a->b is nullable.
self.assertEqual(str(qset.query).count('INNER JOIN'), 1)
+
+class ReverseJoinTrimmingTest(TestCase):
+ def test_reverse_trimming(self):
+ # Check that we don't accidentally trim reverse joins - we can't know
+ # if there is anything on the other side of the join, so trimming
+ # reverse joins can't be done, ever.
+ t = Tag.objects.create()
+ qs = Tag.objects.filter(annotation__tag=t.pk)
+ self.assertIn('INNER JOIN', str(qs.query))
+ self.assertEquals(list(qs), [])
diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py
index 6522620d5f..adf824dff7 100644
--- a/tests/regressiontests/requests/tests.py
+++ b/tests/regressiontests/requests/tests.py
@@ -1,3 +1,4 @@
+# -*- encoding: utf-8 -*-
from __future__ import unicode_literals
import time
@@ -11,7 +12,7 @@ from django.http import HttpRequest, HttpResponse, parse_cookie, build_request_r
from django.test.client import FakePayload
from django.test.utils import override_settings, str_prefix
from django.utils import unittest
-from django.utils.http import cookie_date
+from django.utils.http import cookie_date, urlencode
from django.utils.timezone import utc
@@ -115,13 +116,15 @@ class RequestsTests(unittest.TestCase):
'12.34.56.78:443',
'[2001:19f0:feee::dead:beef:cafe]',
'[2001:19f0:feee::dead:beef:cafe]:8080',
+ 'xn--4ca9at.com', # Punnycode for öäü.com
]
poisoned_hosts = [
'example.com@evil.tld',
'example.com:dr.frankenstein@evil.tld',
- 'example.com:someone@somestie.com:80',
- 'example.com:80/badpath'
+ 'example.com:dr.frankenstein@evil.tld:80',
+ 'example.com:80/badpath',
+ 'example.com: recovermypassword.com',
]
for host in legit_hosts:
@@ -185,13 +188,15 @@ class RequestsTests(unittest.TestCase):
'12.34.56.78:443',
'[2001:19f0:feee::dead:beef:cafe]',
'[2001:19f0:feee::dead:beef:cafe]:8080',
+ 'xn--4ca9at.com', # Punnycode for öäü.com
]
poisoned_hosts = [
'example.com@evil.tld',
'example.com:dr.frankenstein@evil.tld',
'example.com:dr.frankenstein@evil.tld:80',
- 'example.com:80/badpath'
+ 'example.com:80/badpath',
+ 'example.com: recovermypassword.com',
]
for host in legit_hosts:
@@ -352,6 +357,30 @@ class RequestsTests(unittest.TestCase):
self.assertRaises(Exception, lambda: request.body)
self.assertEqual(request.POST, {})
+ def test_non_ascii_POST(self):
+ payload = FakePayload(urlencode({'key': 'España'}))
+ request = WSGIRequest({
+ 'REQUEST_METHOD': 'POST',
+ 'CONTENT_LENGTH': len(payload),
+ 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+ 'wsgi.input': payload,
+ })
+ self.assertEqual(request.POST, {'key': ['España']})
+
+ def test_alternate_charset_POST(self):
+ """
+ Test a POST with non-utf-8 payload encoding.
+ """
+ from django.utils.http import urllib_parse
+ payload = FakePayload(urllib_parse.urlencode({'key': 'España'.encode('latin-1')}))
+ request = WSGIRequest({
+ 'REQUEST_METHOD': 'POST',
+ 'CONTENT_LENGTH': len(payload),
+ 'CONTENT_TYPE': 'application/x-www-form-urlencoded; charset=iso-8859-1',
+ 'wsgi.input': payload,
+ })
+ self.assertEqual(request.POST, {'key': ['España']})
+
def test_body_after_POST_multipart(self):
"""
Reading body after parsing multipart is not allowed
diff --git a/tests/regressiontests/select_related_onetoone/models.py b/tests/regressiontests/select_related_onetoone/models.py
index 3284defb11..d32faafbb9 100644
--- a/tests/regressiontests/select_related_onetoone/models.py
+++ b/tests/regressiontests/select_related_onetoone/models.py
@@ -51,6 +51,7 @@ class StatDetails(models.Model):
class AdvancedUserStat(UserStat):
karma = models.IntegerField()
+
class Image(models.Model):
name = models.CharField(max_length=100)
@@ -58,3 +59,44 @@ class Image(models.Model):
class Product(models.Model):
name = models.CharField(max_length=100)
image = models.OneToOneField(Image, null=True)
+
+
+@python_2_unicode_compatible
+class Parent1(models.Model):
+ name1 = models.CharField(max_length=50)
+
+ def __str__(self):
+ return self.name1
+
+
+@python_2_unicode_compatible
+class Parent2(models.Model):
+ # Avoid having two "id" fields in the Child1 subclass
+ id2 = models.AutoField(primary_key=True)
+ name2 = models.CharField(max_length=50)
+
+ def __str__(self):
+ return self.name2
+
+
+@python_2_unicode_compatible
+class Child1(Parent1, Parent2):
+ value = models.IntegerField()
+
+ def __str__(self):
+ return self.name1
+
+
+@python_2_unicode_compatible
+class Child2(Parent1):
+ parent2 = models.OneToOneField(Parent2)
+ value = models.IntegerField()
+
+ def __str__(self):
+ return self.name1
+
+class Child3(Child2):
+ value3 = models.IntegerField()
+
+class Child4(Child1):
+ value4 = models.IntegerField()
diff --git a/tests/regressiontests/select_related_onetoone/tests.py b/tests/regressiontests/select_related_onetoone/tests.py
index 1373f04717..d4a1275e49 100644
--- a/tests/regressiontests/select_related_onetoone/tests.py
+++ b/tests/regressiontests/select_related_onetoone/tests.py
@@ -1,9 +1,11 @@
from __future__ import absolute_import
from django.test import TestCase
+from django.utils import unittest
from .models import (User, UserProfile, UserStat, UserStatResult, StatDetails,
- AdvancedUserStat, Image, Product)
+ AdvancedUserStat, Image, Product, Parent1, Parent2, Child1, Child2, Child3,
+ Child4)
class ReverseSelectRelatedTestCase(TestCase):
@@ -21,6 +23,14 @@ class ReverseSelectRelatedTestCase(TestCase):
advstat = AdvancedUserStat.objects.create(user=user2, posts=200, karma=5,
results=results2)
StatDetails.objects.create(base_stats=advstat, comments=250)
+ p1 = Parent1(name1="Only Parent1")
+ p1.save()
+ c1 = Child1(name1="Child1 Parent1", name2="Child1 Parent2", value=1)
+ c1.save()
+ p2 = Parent2(name2="Child2 Parent2")
+ p2.save()
+ c2 = Child2(name1="Child2 Parent1", parent2=p2, value=2)
+ c2.save()
def test_basic(self):
with self.assertNumQueries(1):
@@ -108,3 +118,93 @@ class ReverseSelectRelatedTestCase(TestCase):
image = Image.objects.select_related('product').get()
with self.assertRaises(Product.DoesNotExist):
image.product
+
+ def test_parent_only(self):
+ with self.assertNumQueries(1):
+ p = Parent1.objects.select_related('child1').get(name1="Only Parent1")
+ with self.assertNumQueries(0):
+ with self.assertRaises(Child1.DoesNotExist):
+ p.child1
+
+ def test_multiple_subclass(self):
+ with self.assertNumQueries(1):
+ p = Parent1.objects.select_related('child1').get(name1="Child1 Parent1")
+ self.assertEqual(p.child1.name2, 'Child1 Parent2')
+
+ def test_onetoone_with_subclass(self):
+ with self.assertNumQueries(1):
+ p = Parent2.objects.select_related('child2').get(name2="Child2 Parent2")
+ self.assertEqual(p.child2.name1, 'Child2 Parent1')
+
+ def test_onetoone_with_two_subclasses(self):
+ with self.assertNumQueries(1):
+ p = Parent2.objects.select_related('child2', "child2__child3").get(name2="Child2 Parent2")
+ self.assertEqual(p.child2.name1, 'Child2 Parent1')
+ with self.assertRaises(Child3.DoesNotExist):
+ p.child2.child3
+ p3 = Parent2(name2="Child3 Parent2")
+ p3.save()
+ c2 = Child3(name1="Child3 Parent1", parent2=p3, value=2, value3=3)
+ c2.save()
+ with self.assertNumQueries(1):
+ p = Parent2.objects.select_related('child2', "child2__child3").get(name2="Child3 Parent2")
+ self.assertEqual(p.child2.name1, 'Child3 Parent1')
+ self.assertEqual(p.child2.child3.value3, 3)
+ self.assertEqual(p.child2.child3.value, p.child2.value)
+ self.assertEqual(p.child2.name1, p.child2.child3.name1)
+
+ def test_multiinheritance_two_subclasses(self):
+ with self.assertNumQueries(1):
+ p = Parent1.objects.select_related('child1', 'child1__child4').get(name1="Child1 Parent1")
+ self.assertEqual(p.child1.name2, 'Child1 Parent2')
+ self.assertEqual(p.child1.name1, p.name1)
+ with self.assertRaises(Child4.DoesNotExist):
+ p.child1.child4
+ Child4(name1='n1', name2='n2', value=1, value4=4).save()
+ with self.assertNumQueries(1):
+ p = Parent2.objects.select_related('child1', 'child1__child4').get(name2="n2")
+ self.assertEqual(p.name2, 'n2')
+ self.assertEqual(p.child1.name1, 'n1')
+ self.assertEqual(p.child1.name2, p.name2)
+ self.assertEqual(p.child1.value, 1)
+ self.assertEqual(p.child1.child4.name1, p.child1.name1)
+ self.assertEqual(p.child1.child4.name2, p.child1.name2)
+ self.assertEqual(p.child1.child4.value, p.child1.value)
+ self.assertEqual(p.child1.child4.value4, 4)
+
+ @unittest.expectedFailure
+ def test_inheritance_deferred(self):
+ c = Child4.objects.create(name1='n1', name2='n2', value=1, value4=4)
+ with self.assertNumQueries(1):
+ p = Parent2.objects.select_related('child1').only(
+ 'id2', 'child1__value').get(name2="n2")
+ self.assertEqual(p.id2, c.id2)
+ self.assertEqual(p.child1.value, 1)
+ p = Parent2.objects.select_related('child1').only(
+ 'id2', 'child1__value').get(name2="n2")
+ with self.assertNumQueries(1):
+ self.assertEquals(p.name2, 'n2')
+ p = Parent2.objects.select_related('child1').only(
+ 'id2', 'child1__value').get(name2="n2")
+ with self.assertNumQueries(1):
+ self.assertEquals(p.child1.name2, 'n2')
+
+ @unittest.expectedFailure
+ def test_inheritance_deferred2(self):
+ c = Child4.objects.create(name1='n1', name2='n2', value=1, value4=4)
+ qs = Parent2.objects.select_related('child1', 'child4').only(
+ 'id2', 'child1__value', 'child1__child4__value4')
+ with self.assertNumQueries(1):
+ p = qs.get(name2="n2")
+ self.assertEqual(p.id2, c.id2)
+ self.assertEqual(p.child1.value, 1)
+ self.assertEqual(p.child1.child4.value4, 4)
+ self.assertEqual(p.child1.child4.id2, c.id2)
+ p = qs.get(name2="n2")
+ with self.assertNumQueries(1):
+ self.assertEquals(p.child1.name2, 'n2')
+ p = qs.get(name2="n2")
+ with self.assertNumQueries(1):
+ self.assertEquals(p.child1.name1, 'n1')
+ with self.assertNumQueries(1):
+ self.assertEquals(p.child1.child4.name1, 'n1')
diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py
index 52223e72d8..af261f9cbc 100644
--- a/tests/regressiontests/serializers_regress/models.py
+++ b/tests/regressiontests/serializers_regress/models.py
@@ -8,7 +8,6 @@ This class sets up a model for each model field type
from django.db import models
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
-from django.contrib.localflavor.us.models import USStateField, PhoneNumberField
# The following classes are for testing basic data
# marshalling, including NULL values, where allowed.
@@ -58,9 +57,6 @@ class GenericIPAddressData(models.Model):
class NullBooleanData(models.Model):
data = models.NullBooleanField(null=True)
-class PhoneData(models.Model):
- data = PhoneNumberField(null=True)
-
class PositiveIntegerData(models.Model):
data = models.PositiveIntegerField(null=True)
@@ -79,9 +75,6 @@ class TextData(models.Model):
class TimeData(models.Model):
data = models.TimeField(null=True)
-class USStateData(models.Model):
- data = USStateField(null=True)
-
class Tag(models.Model):
"""A tag on an item."""
data = models.SlugField()
@@ -212,9 +205,6 @@ class GenericIPAddressPKData(models.Model):
# class NullBooleanPKData(models.Model):
# data = models.NullBooleanField(primary_key=True)
-class PhonePKData(models.Model):
- data = PhoneNumberField(primary_key=True)
-
class PositiveIntegerPKData(models.Model):
data = models.PositiveIntegerField(primary_key=True)
@@ -233,9 +223,6 @@ class SmallPKData(models.Model):
# class TimePKData(models.Model):
# data = models.TimeField(primary_key=True)
-class USStatePKData(models.Model):
- data = USStateField(primary_key=True)
-
class ComplexModel(models.Model):
field1 = models.CharField(max_length=10)
field2 = models.CharField(max_length=10)
diff --git a/tests/regressiontests/serializers_regress/tests.py b/tests/regressiontests/serializers_regress/tests.py
index f4b3adaa5e..2583a1ea04 100644
--- a/tests/regressiontests/serializers_regress/tests.py
+++ b/tests/regressiontests/serializers_regress/tests.py
@@ -28,14 +28,14 @@ from django.utils.unittest import skipUnless
from .models import (BooleanData, CharData, DateData, DateTimeData, EmailData,
FileData, FilePathData, DecimalData, FloatData, IntegerData, IPAddressData,
- GenericIPAddressData, NullBooleanData, PhoneData, PositiveIntegerData,
+ GenericIPAddressData, NullBooleanData, PositiveIntegerData,
PositiveSmallIntegerData, SlugData, SmallData, TextData, TimeData,
- USStateData, GenericData, Anchor, UniqueAnchor, FKData, M2MData, O2OData,
+ GenericData, Anchor, UniqueAnchor, FKData, M2MData, O2OData,
FKSelfData, M2MSelfData, FKDataToField, FKDataToO2O, M2MIntermediateData,
Intermediate, BooleanPKData, CharPKData, EmailPKData, FilePathPKData,
DecimalPKData, FloatPKData, IntegerPKData, IPAddressPKData,
- GenericIPAddressPKData, PhonePKData, PositiveIntegerPKData,
- PositiveSmallIntegerPKData, SlugPKData, SmallPKData, USStatePKData,
+ GenericIPAddressPKData, PositiveIntegerPKData,
+ PositiveSmallIntegerPKData, SlugPKData, SmallPKData,
AutoNowDateTimeData, ModifyingSaveData, InheritAbstractModel, BaseModel,
ExplicitInheritBaseModel, InheritBaseModel, ProxyBaseModel,
ProxyProxyBaseModel, BigIntegerData, LengthModel, Tag, ComplexModel,
@@ -218,8 +218,6 @@ test_data = [
(data_obj, 100, NullBooleanData, True),
(data_obj, 101, NullBooleanData, False),
(data_obj, 102, NullBooleanData, None),
- (data_obj, 110, PhoneData, "212-634-5789"),
- (data_obj, 111, PhoneData, None),
(data_obj, 120, PositiveIntegerData, 123456789),
(data_obj, 121, PositiveIntegerData, None),
(data_obj, 130, PositiveSmallIntegerData, 12),
@@ -239,9 +237,6 @@ The end."""),
(data_obj, 162, TextData, None),
(data_obj, 170, TimeData, datetime.time(10,42,37)),
(data_obj, 171, TimeData, None),
- (data_obj, 180, USStateData, "MA"),
- (data_obj, 181, USStateData, None),
- (data_obj, 182, USStateData, ""),
(generic_obj, 200, GenericData, ['Generic Object 1', 'tag1', 'tag2']),
(generic_obj, 201, GenericData, ['Generic Object 2', 'tag2', 'tag3']),
@@ -320,7 +315,6 @@ The end."""),
(pk_obj, 695, GenericIPAddressPKData, "fe80:1424:2223:6cff:fe8a:2e8a:2151:abcd"),
# (pk_obj, 700, NullBooleanPKData, True),
# (pk_obj, 701, NullBooleanPKData, False),
- (pk_obj, 710, PhonePKData, "212-634-5789"),
(pk_obj, 720, PositiveIntegerPKData, 123456789),
(pk_obj, 730, PositiveSmallIntegerPKData, 12),
(pk_obj, 740, SlugPKData, "this-is-a-slug"),
@@ -332,7 +326,6 @@ The end."""),
# Several of them.
# The end."""),
# (pk_obj, 770, TimePKData, datetime.time(10,42,37)),
- (pk_obj, 780, USStatePKData, "MA"),
# (pk_obj, 790, XMLPKData, "<foo></foo>"),
(data_obj, 800, AutoNowDateTimeData, datetime.datetime(2006,6,16,10,42,37)),
diff --git a/tests/regressiontests/servers/tests.py b/tests/regressiontests/servers/tests.py
index f54e34ce28..1a7552ed11 100644
--- a/tests/regressiontests/servers/tests.py
+++ b/tests/regressiontests/servers/tests.py
@@ -15,11 +15,12 @@ from django.test import LiveServerTestCase
from django.core.servers.basehttp import WSGIServerException
from django.test.utils import override_settings
from django.utils.http import urlencode
+from django.utils._os import upath
from .models import Person
-TEST_ROOT = os.path.dirname(__file__)
+TEST_ROOT = os.path.dirname(upath(__file__))
TEST_SETTINGS = {
'MEDIA_URL': '/media/',
'MEDIA_ROOT': os.path.join(TEST_ROOT, 'media'),
diff --git a/tests/regressiontests/settings_tests/tests.py b/tests/regressiontests/settings_tests/tests.py
index aaf8bcffcf..68f620b5c9 100644
--- a/tests/regressiontests/settings_tests/tests.py
+++ b/tests/regressiontests/settings_tests/tests.py
@@ -4,8 +4,9 @@ import warnings
from django.conf import settings, global_settings
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest
-from django.test import TransactionTestCase, TestCase, signals
+from django.test import SimpleTestCase, TransactionTestCase, TestCase, signals
from django.test.utils import override_settings
+from django.utils import unittest, six
@override_settings(TEST='override')
@@ -67,11 +68,6 @@ class ClassDecoratedTestCase(ClassDecoratedTestCaseSuper):
self.fail()
-class SettingGetter(object):
- def __init__(self):
- self.test = getattr(settings, 'TEST', 'undefined')
-
-
class SettingsTests(TestCase):
def setUp(self):
self.testvalue = None
@@ -122,10 +118,20 @@ class SettingsTests(TestCase):
self.assertRaises(AttributeError, getattr, settings, 'TEST')
def test_class_decorator(self):
- self.assertEqual(SettingGetter().test, 'undefined')
- DecoratedSettingGetter = override_settings(TEST='override')(SettingGetter)
- self.assertEqual(DecoratedSettingGetter().test, 'override')
- self.assertRaises(AttributeError, getattr, settings, 'TEST')
+ # SimpleTestCase can be decorated by override_settings, but not ut.TestCase
+ class SimpleTestCaseSubclass(SimpleTestCase):
+ pass
+
+ class UnittestTestCaseSubclass(unittest.TestCase):
+ pass
+
+ decorated = override_settings(TEST='override')(SimpleTestCaseSubclass)
+ self.assertIsInstance(decorated, type)
+ self.assertTrue(issubclass(decorated, SimpleTestCase))
+
+ with six.assertRaisesRegex(self, Exception,
+ "Only subclasses of Django SimpleTestCase*"):
+ decorated = override_settings(TEST='override')(UnittestTestCaseSubclass)
def test_signal_callback_context_manager(self):
self.assertRaises(AttributeError, getattr, settings, 'TEST')
diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py
index 7ecbccc448..90c8621d0b 100644
--- a/tests/regressiontests/staticfiles_tests/tests.py
+++ b/tests/regressiontests/staticfiles_tests/tests.py
@@ -12,18 +12,17 @@ from django.template import loader, Context
from django.conf import settings
from django.core.cache.backends.base import BaseCache
from django.core.exceptions import ImproperlyConfigured
-from django.core.files.storage import default_storage
from django.core.management import call_command
from django.test import TestCase
from django.test.utils import override_settings
-from django.utils.encoding import smart_text
+from django.utils.encoding import force_text
from django.utils.functional import empty
-from django.utils._os import rmtree_errorhandler
+from django.utils._os import rmtree_errorhandler, upath
from django.utils import six
from django.contrib.staticfiles import finders, storage
-TEST_ROOT = os.path.dirname(__file__)
+TEST_ROOT = os.path.dirname(upath(__file__))
TEST_SETTINGS = {
'DEBUG': True,
'MEDIA_URL': '/media/',
@@ -48,10 +47,9 @@ class BaseStaticFilesTestCase(object):
Test case with a couple utility assertions.
"""
def setUp(self):
- # Clear the cached default_storage out, this is because when it first
- # gets accessed (by some other test), it evaluates settings.MEDIA_ROOT,
+ # Clear the cached staticfiles_storage out, this is because when it first
+ # gets accessed (by some other test), it evaluates settings.STATIC_ROOT,
# since we're planning on changing that we need to clear out the cache.
- default_storage._wrapped = empty
storage.staticfiles_storage._wrapped = empty
# Clear the cached staticfile finders, so they are reinitialized every
# run and pick up changes in settings.STATICFILES_DIRS.
@@ -60,9 +58,9 @@ class BaseStaticFilesTestCase(object):
testfiles_path = os.path.join(TEST_ROOT, 'apps', 'test', 'static', 'test')
# To make sure SVN doesn't hangs itself with the non-ASCII characters
# during checkout, we actually create one file dynamically.
- self._nonascii_filepath = os.path.join(testfiles_path, 'fi\u015fier.txt')
+ self._nonascii_filepath = os.path.join(testfiles_path, '\u2297.txt')
with codecs.open(self._nonascii_filepath, 'w', 'utf-8') as f:
- f.write("fi\u015fier in the app dir")
+ f.write("\u2297 in the app dir")
# And also create the stupid hidden file to dwarf the setup.py's
# package data handling.
self._hidden_filepath = os.path.join(testfiles_path, '.hidden')
@@ -79,7 +77,7 @@ class BaseStaticFilesTestCase(object):
os.unlink(self._backup_filepath)
def assertFileContains(self, filepath, text):
- self.assertIn(text, self._get_file(smart_text(filepath)),
+ self.assertIn(text, self._get_file(force_text(filepath)),
"'%s' not in '%s'" % (text, filepath))
def assertFileNotFound(self, filepath):
@@ -179,7 +177,7 @@ class TestDefaults(object):
"""
Can find a file with non-ASCII character in an app static/ directory.
"""
- self.assertFileContains('test/fişier.txt', 'fişier in the app dir')
+ self.assertFileContains('test/⊗.txt', '⊗ in the app dir')
def test_camelcase_filenames(self):
"""
@@ -197,7 +195,7 @@ class TestFindStatic(CollectionTestCase, TestDefaults):
call_command('findstatic', filepath, all=False, verbosity=0, stdout=out)
out.seek(0)
lines = [l.strip() for l in out.readlines()]
- with codecs.open(smart_text(lines[1].strip()), "r", "utf-8") as f:
+ with codecs.open(force_text(lines[1].strip()), "r", "utf-8") as f:
return f.read()
def test_all_files(self):
@@ -209,8 +207,8 @@ class TestFindStatic(CollectionTestCase, TestDefaults):
out.seek(0)
lines = [l.strip() for l in out.readlines()]
self.assertEqual(len(lines), 3) # three because there is also the "Found <file> here" line
- self.assertIn('project', lines[1])
- self.assertIn('apps', lines[2])
+ self.assertIn('project', force_text(lines[1]))
+ self.assertIn('apps', force_text(lines[2]))
class TestCollection(CollectionTestCase, TestDefaults):
@@ -709,9 +707,6 @@ class TestMiscFinder(TestCase):
"""
A few misc finder tests.
"""
- def setUp(self):
- default_storage._wrapped = empty
-
def test_get_finder(self):
self.assertIsInstance(finders.get_finder(
'django.contrib.staticfiles.finders.FileSystemFinder'),
diff --git a/tests/regressiontests/swappable_models/__init__.py b/tests/regressiontests/swappable_models/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/swappable_models/__init__.py
diff --git a/tests/regressiontests/swappable_models/models.py b/tests/regressiontests/swappable_models/models.py
new file mode 100644
index 0000000000..92692d4396
--- /dev/null
+++ b/tests/regressiontests/swappable_models/models.py
@@ -0,0 +1,15 @@
+from django.db import models
+
+
+class Article(models.Model):
+ title = models.CharField(max_length=100)
+ publication_date = models.DateField()
+
+ class Meta:
+ swappable = 'TEST_ARTICLE_MODEL'
+
+
+class AlternateArticle(models.Model):
+ title = models.CharField(max_length=100)
+ publication_date = models.DateField()
+ byline = models.CharField(max_length=100)
diff --git a/tests/regressiontests/swappable_models/tests.py b/tests/regressiontests/swappable_models/tests.py
new file mode 100644
index 0000000000..75644e32aa
--- /dev/null
+++ b/tests/regressiontests/swappable_models/tests.py
@@ -0,0 +1,46 @@
+from __future__ import absolute_import, unicode_literals
+
+from django.utils.six import StringIO
+
+from django.contrib.auth.models import Permission
+from django.contrib.contenttypes.models import ContentType
+from django.core import management
+from django.db.models.loading import cache
+from django.test import TestCase
+from django.test.utils import override_settings
+
+
+class SwappableModelTests(TestCase):
+ def setUp(self):
+ # This test modifies the installed apps, so we need to make sure
+ # we're not dealing with a cached app list.
+ cache._get_models_cache.clear()
+
+ def tearDown(self):
+ # By fiddling with swappable models, we alter the installed models
+ # cache, so flush it to make sure there are no side effects.
+ cache._get_models_cache.clear()
+
+ @override_settings(TEST_ARTICLE_MODEL='swappable_models.AlternateArticle')
+ def test_generated_data(self):
+ "Permissions and content types are not created for a swapped model"
+
+ # Delete all permissions and content_types
+ Permission.objects.filter(content_type__app_label='swappable_models').delete()
+ ContentType.objects.filter(app_label='swappable_models').delete()
+
+ # Re-run syncdb. This will re-build the permissions and content types.
+ new_io = StringIO()
+ management.call_command('syncdb', load_initial_data=False, interactive=False, stdout=new_io)
+
+ # Check that content types and permissions exist for the swapped model,
+ # but not for the swappable model.
+ apps_models = [(p.content_type.app_label, p.content_type.model)
+ for p in Permission.objects.all()]
+ self.assertIn(('swappable_models', 'alternatearticle'), apps_models)
+ self.assertNotIn(('swappable_models', 'article'), apps_models)
+
+ apps_models = [(ct.app_label, ct.model)
+ for ct in ContentType.objects.all()]
+ self.assertIn(('swappable_models', 'alternatearticle'), apps_models)
+ self.assertNotIn(('swappable_models', 'article'), apps_models)
diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py
index 8b4aebbf22..7ba1681fd5 100644
--- a/tests/regressiontests/templates/filters.py
+++ b/tests/regressiontests/templates/filters.py
@@ -8,7 +8,7 @@ consistent.
"""
from __future__ import unicode_literals
-from datetime import date, datetime, timedelta
+from datetime import date, datetime, time, timedelta
from django.test.utils import str_prefix
from django.utils.tzinfo import LocalTimezone, FixedOffset
@@ -356,6 +356,9 @@ def get_filter_tests():
# Timezone name
'date06': (r'{{ d|date:"e" }}', {'d': datetime(2009, 3, 12, tzinfo=FixedOffset(30))}, '+0030'),
'date07': (r'{{ d|date:"e" }}', {'d': datetime(2009, 3, 12)}, ''),
+ # Ticket 19370: Make sure |date doesn't blow up on a midnight time object
+ 'date08': (r'{{ t|date:"H:i" }}', {'t': time(0, 1)}, '00:01'),
+ 'date09': (r'{{ t|date:"H:i" }}', {'t': time(0, 0)}, '00:00'),
# Tests for #11687 and #16676
'add01': (r'{{ i|add:"5" }}', {'i': 2000}, '2005'),
diff --git a/tests/regressiontests/templates/loaders.py b/tests/regressiontests/templates/loaders.py
index 7fbb0841f9..b77965203f 100644
--- a/tests/regressiontests/templates/loaders.py
+++ b/tests/regressiontests/templates/loaders.py
@@ -18,6 +18,7 @@ from django.template import TemplateDoesNotExist, Context
from django.template.loaders.eggs import Loader as EggLoader
from django.template import loader
from django.utils import unittest, six
+from django.utils._os import upath
from django.utils.six import StringIO
@@ -111,9 +112,9 @@ class CachedLoader(unittest.TestCase):
def test_templatedir_caching(self):
"Check that the template directories form part of the template cache key. Refs #13573"
# Retrive a template specifying a template directory to check
- t1, name = loader.find_template('test.html', (os.path.join(os.path.dirname(__file__), 'templates', 'first'),))
+ t1, name = loader.find_template('test.html', (os.path.join(os.path.dirname(upath(__file__)), 'templates', 'first'),))
# Now retrieve the same template name, but from a different directory
- t2, name = loader.find_template('test.html', (os.path.join(os.path.dirname(__file__), 'templates', 'second'),))
+ t2, name = loader.find_template('test.html', (os.path.join(os.path.dirname(upath(__file__)), 'templates', 'second'),))
# The two templates should not have the same content
self.assertNotEqual(t1.render(Context({})), t2.render(Context({})))
@@ -123,7 +124,7 @@ class RenderToStringTest(unittest.TestCase):
def setUp(self):
self._old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS
settings.TEMPLATE_DIRS = (
- os.path.join(os.path.dirname(__file__), 'templates'),
+ os.path.join(os.path.dirname(upath(__file__)), 'templates'),
)
def tearDown(self):
diff --git a/tests/regressiontests/templates/response.py b/tests/regressiontests/templates/response.py
index 93919b95cd..c4da50af6b 100644
--- a/tests/regressiontests/templates/response.py
+++ b/tests/regressiontests/templates/response.py
@@ -11,6 +11,7 @@ from django.template import Template, Context
from django.template.response import (TemplateResponse, SimpleTemplateResponse,
ContentNotRenderedError)
from django.test.utils import override_settings
+from django.utils._os import upath
def test_processor(request):
return {'processors': 'yes'}
@@ -189,9 +190,24 @@ class SimpleTemplateResponseTest(TestCase):
unpickled_response = pickle.loads(pickled_response)
repickled_response = pickle.dumps(unpickled_response)
+ def test_pickling_cookie(self):
+ response = SimpleTemplateResponse('first/test.html', {
+ 'value': 123,
+ 'fn': datetime.now,
+ })
+
+ response.cookies['key'] = 'value'
+
+ response.render()
+ pickled_response = pickle.dumps(response, pickle.HIGHEST_PROTOCOL)
+ unpickled_response = pickle.loads(pickled_response)
+
+ self.assertEqual(unpickled_response.cookies['key'].value, 'value')
+
+
@override_settings(
TEMPLATE_CONTEXT_PROCESSORS=[test_processor_name],
- TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__),'templates')),
+ TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'templates')),
)
class TemplateResponseTest(TestCase):
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index c95b5bfa3c..d21434a12e 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -19,16 +19,18 @@ except ImportError: # Python 2
from urlparse import urljoin
from django import template
-from django.template import base as template_base, RequestContext, Template, Context
+from django.template import (base as template_base, Context, RequestContext,
+ Template)
from django.core import urlresolvers
from django.template import loader
from django.template.loaders import app_directories, filesystem, cached
-from django.test import RequestFactory
+from django.test import RequestFactory, TestCase
from django.test.utils import (setup_test_template_loader,
restore_template_loaders, override_settings)
from django.utils import unittest
from django.utils.encoding import python_2_unicode_compatible
from django.utils.formats import date_format
+from django.utils._os import upath
from django.utils.translation import activate, deactivate, ugettext as _
from django.utils.safestring import mark_safe
from django.utils import six
@@ -156,7 +158,7 @@ class UTF8Class:
return 'ŠĐĆŽćžšđ'
@override_settings(MEDIA_URL="/media/", STATIC_URL="/static/")
-class Templates(unittest.TestCase):
+class Templates(TestCase):
def test_loaders_security(self):
ad_loader = app_directories.Loader()
@@ -364,6 +366,20 @@ class Templates(unittest.TestCase):
with self.assertRaises(urlresolvers.NoReverseMatch):
t.render(c)
+ def test_url_explicit_exception_for_old_syntax_at_run_time(self):
+ # Regression test for #19280
+ t = Template('{% url path.to.view %}') # not quoted = old syntax
+ c = Context()
+ with self.assertRaisesRegexp(urlresolvers.NoReverseMatch,
+ "The syntax changed in Django 1.5, see the docs."):
+ t.render(c)
+
+ def test_url_explicit_exception_for_old_syntax_at_compile_time(self):
+ # Regression test for #19392
+ with self.assertRaisesRegexp(template.TemplateSyntaxError,
+ "The syntax of 'url' changed in Django 1.5, see the docs."):
+ t = Template('{% url my-view %}') # not a variable = old syntax
+
@override_settings(DEBUG=True, TEMPLATE_DEBUG=True)
def test_no_wrapped_exception(self):
"""
@@ -414,7 +430,7 @@ class Templates(unittest.TestCase):
# Set ALLOWED_INCLUDE_ROOTS so that ssi works.
old_allowed_include_roots = settings.ALLOWED_INCLUDE_ROOTS
settings.ALLOWED_INCLUDE_ROOTS = (
- os.path.dirname(os.path.abspath(__file__)),
+ os.path.dirname(os.path.abspath(upath(__file__))),
)
# Warm the URL reversing cache. This ensures we don't pay the cost
@@ -505,7 +521,7 @@ class Templates(unittest.TestCase):
def get_template_tests(self):
# SYNTAX --
# 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
- basedir = os.path.dirname(os.path.abspath(__file__))
+ basedir = os.path.dirname(os.path.abspath(upath(__file__)))
tests = {
### BASIC SYNTAX ################################################
@@ -1305,7 +1321,7 @@ class Templates(unittest.TestCase):
# retrieving language information
'i18n28_2': ('{% load i18n %}{% get_language_info for "de" as l %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}', {}, 'de: German/Deutsch bidi=False'),
'i18n29': ('{% load i18n %}{% get_language_info for LANGUAGE_CODE as l %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}', {'LANGUAGE_CODE': 'fi'}, 'fi: Finnish/suomi bidi=False'),
- 'i18n30': ('{% load i18n %}{% get_language_info_list for langcodes as langs %}{% for l in langs %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}; {% endfor %}', {'langcodes': ['it', 'no']}, 'it: Italian/italiano bidi=False; no: Norwegian/Norsk bidi=False; '),
+ 'i18n30': ('{% load i18n %}{% get_language_info_list for langcodes as langs %}{% for l in langs %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}; {% endfor %}', {'langcodes': ['it', 'no']}, 'it: Italian/italiano bidi=False; no: Norwegian/norsk bidi=False; '),
'i18n31': ('{% load i18n %}{% get_language_info_list for langcodes as langs %}{% for l in langs %}{{ l.code }}: {{ l.name }}/{{ l.name_local }} bidi={{ l.bidi }}; {% endfor %}', {'langcodes': (('sl', 'Slovenian'), ('fa', 'Persian'))}, 'sl: Slovenian/Sloven\u0161\u010dina bidi=False; fa: Persian/\u0641\u0627\u0631\u0633\u06cc bidi=True; '),
'i18n32': ('{% load i18n %}{{ "hu"|language_name }} {{ "hu"|language_name_local }} {{ "hu"|language_bidi }}', {}, 'Hungarian Magyar False'),
'i18n33': ('{% load i18n %}{{ langcode|language_name }} {{ langcode|language_name_local }} {{ langcode|language_bidi }}', {'langcode': 'nl'}, 'Dutch Nederlands False'),
@@ -1640,7 +1656,7 @@ class TemplateTagLoading(unittest.TestCase):
def setUp(self):
self.old_path = sys.path[:]
self.old_apps = settings.INSTALLED_APPS
- self.egg_dir = '%s/eggs' % os.path.dirname(__file__)
+ self.egg_dir = '%s/eggs' % os.path.dirname(upath(__file__))
self.old_tag_modules = template_base.templatetags_modules
template_base.templatetags_modules = []
diff --git a/tests/regressiontests/test_client_regress/tests.py b/tests/regressiontests/test_client_regress/tests.py
index f424321663..5ba5d3c4b3 100644
--- a/tests/regressiontests/test_client_regress/tests.py
+++ b/tests/regressiontests/test_client_regress/tests.py
@@ -16,12 +16,13 @@ from django.test import Client, TestCase
from django.test.client import encode_file, RequestFactory
from django.test.utils import ContextList, override_settings, str_prefix
from django.template.response import SimpleTemplateResponse
+from django.utils._os import upath
from django.utils.translation import ugettext_lazy
from django.http import HttpResponse
@override_settings(
- TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),)
+ TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'templates'),)
)
class AssertContainsTests(TestCase):
def test_contains(self):
@@ -629,7 +630,7 @@ class TemplateExceptionTests(TestCase):
template_loader.reset()
@override_settings(
- TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'bad_templates'),)
+ TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'bad_templates'),)
)
def test_bad_404_template(self):
"Errors found when rendering 404 error templates are re-raised"
diff --git a/tests/regressiontests/test_utils/models.py b/tests/regressiontests/test_utils/models.py
index 4da7a07bbf..85a1031c02 100644
--- a/tests/regressiontests/test_utils/models.py
+++ b/tests/regressiontests/test_utils/models.py
@@ -1,5 +1,9 @@
from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
-
+@python_2_unicode_compatible
class Person(models.Model):
name = models.CharField(max_length=100)
+
+ def __str__(self):
+ return self.name
diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py
index 95913b5aab..d5d49b2104 100644
--- a/tests/regressiontests/test_utils/tests.py
+++ b/tests/regressiontests/test_utils/tests.py
@@ -54,6 +54,46 @@ class AssertNumQueriesTests(TestCase):
self.assertNumQueries(2, test_func)
+class AssertQuerysetEqualTests(TestCase):
+ def setUp(self):
+ self.p1 = Person.objects.create(name='p1')
+ self.p2 = Person.objects.create(name='p2')
+
+ def test_ordered(self):
+ self.assertQuerysetEqual(
+ Person.objects.all().order_by('name'),
+ [repr(self.p1), repr(self.p2)]
+ )
+
+ def test_unordered(self):
+ self.assertQuerysetEqual(
+ Person.objects.all().order_by('name'),
+ [repr(self.p2), repr(self.p1)],
+ ordered=False
+ )
+
+ def test_transform(self):
+ self.assertQuerysetEqual(
+ Person.objects.all().order_by('name'),
+ [self.p1.pk, self.p2.pk],
+ transform=lambda x: x.pk
+ )
+
+ def test_undefined_order(self):
+ # Using an unordered queryset with more than one ordered value
+ # is an error.
+ with self.assertRaises(ValueError):
+ self.assertQuerysetEqual(
+ Person.objects.all(),
+ [repr(self.p1), repr(self.p2)]
+ )
+ # No error for one value.
+ self.assertQuerysetEqual(
+ Person.objects.filter(name='p1'),
+ [repr(self.p1)]
+ )
+
+
class AssertNumQueriesContextManagerTests(TestCase):
urls = 'regressiontests.test_utils.urls'
diff --git a/tests/regressiontests/transactions_regress/tests.py b/tests/regressiontests/transactions_regress/tests.py
index 472e2aafd9..5d1ab2c6f6 100644
--- a/tests/regressiontests/transactions_regress/tests.py
+++ b/tests/regressiontests/transactions_regress/tests.py
@@ -24,17 +24,15 @@ class TestTransactionClosing(TransactionTestCase):
def raw_sql():
"Write a record using raw sql under a commit_on_success decorator"
cursor = connection.cursor()
- cursor.execute("INSERT into transactions_regress_mod (id,fld) values (17,18)")
+ cursor.execute("INSERT into transactions_regress_mod (fld) values (18)")
raw_sql()
# Rollback so that if the decorator didn't commit, the record is unwritten
transaction.rollback()
- try:
- # Check that the record is in the DB
- obj = Mod.objects.get(pk=17)
- self.assertEqual(obj.fld, 18)
- except Mod.DoesNotExist:
- self.fail("transaction with raw sql not committed")
+ self.assertEqual(Mod.objects.count(), 1)
+ # Check that the record is in the DB
+ obj = Mod.objects.all()[0]
+ self.assertEqual(obj.fld, 18)
def test_commit_manually_enforced(self):
"""
@@ -115,19 +113,16 @@ class TestTransactionClosing(TransactionTestCase):
be committed.
"""
cursor = connection.cursor()
- cursor.execute("INSERT into transactions_regress_mod (id,fld) values (1,2)")
+ cursor.execute("INSERT into transactions_regress_mod (fld) values (2)")
transaction.rollback()
- cursor.execute("INSERT into transactions_regress_mod (id,fld) values (1,2)")
+ cursor.execute("INSERT into transactions_regress_mod (fld) values (2)")
reuse_cursor_ref()
# Rollback so that if the decorator didn't commit, the record is unwritten
transaction.rollback()
- try:
- # Check that the record is in the DB
- obj = Mod.objects.get(pk=1)
- self.assertEqual(obj.fld, 2)
- except Mod.DoesNotExist:
- self.fail("After ending a transaction, cursor use no longer sets dirty")
+ self.assertEqual(Mod.objects.count(), 1)
+ obj = Mod.objects.all()[0]
+ self.assertEqual(obj.fld, 2)
def test_failing_query_transaction_closed(self):
"""
@@ -145,7 +140,7 @@ class TestTransactionClosing(TransactionTestCase):
"Create a user in a transaction"
user = User.objects.create_user(username='system', password='iamr00t', email='root@SITENAME.com')
# Redundant, just makes sure the user id was read back from DB
- Mod.objects.create(fld=user.id)
+ Mod.objects.create(fld=user.pk)
# Create a user
create_system_user()
diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py
index 234897d267..eb3afe8201 100644
--- a/tests/regressiontests/urlpatterns_reverse/tests.py
+++ b/tests/regressiontests/urlpatterns_reverse/tests.py
@@ -171,6 +171,18 @@ class URLPatternReverse(TestCase):
# Reversing None should raise an error, not return the last un-named view.
self.assertRaises(NoReverseMatch, reverse, None)
+ def test_prefix_braces(self):
+ self.assertEqual('/%7B%7Binvalid%7D%7D/includes/non_path_include/',
+ reverse('non_path_include', prefix='/{{invalid}}/'))
+
+ def test_prefix_parenthesis(self):
+ self.assertEqual('/bogus%29/includes/non_path_include/',
+ reverse('non_path_include', prefix='/bogus)/'))
+
+ def test_prefix_format_char(self):
+ self.assertEqual('/bump%2520map/includes/non_path_include/',
+ reverse('non_path_include', prefix='/bump%20map/'))
+
class ResolverTests(unittest.TestCase):
def test_resolver_repr(self):
"""
@@ -225,7 +237,7 @@ class ResolverTests(unittest.TestCase):
self.assertEqual(len(e.args[0]['tried']), len(url_types_names), 'Wrong number of tried URLs returned. Expected %s, got %s.' % (len(url_types_names), len(e.args[0]['tried'])))
for tried, expected in zip(e.args[0]['tried'], url_types_names):
for t, e in zip(tried, expected):
- self.assertTrue(isinstance(t, e['type']), '%s is not an instance of %s' % (t, e['type']))
+ self.assertTrue(isinstance(t, e['type']), str('%s is not an instance of %s') % (t, e['type']))
if 'name' in e:
if not e['name']:
self.assertTrue(t.name is None, 'Expected no URL name but found %s.' % t.name)
diff --git a/tests/regressiontests/utils/archive.py b/tests/regressiontests/utils/archive.py
index 5575f340f6..8861b4a577 100644
--- a/tests/regressiontests/utils/archive.py
+++ b/tests/regressiontests/utils/archive.py
@@ -4,9 +4,10 @@ import tempfile
from django.utils import unittest
from django.utils.archive import Archive, extract
+from django.utils._os import upath
-TEST_DIR = os.path.join(os.path.dirname(__file__), 'archives')
+TEST_DIR = os.path.join(os.path.dirname(upath(__file__)), 'archives')
class ArchiveTester(object):
diff --git a/tests/regressiontests/utils/feedgenerator.py b/tests/regressiontests/utils/feedgenerator.py
index b646a5997d..bcd53bb2a0 100644
--- a/tests/regressiontests/utils/feedgenerator.py
+++ b/tests/regressiontests/utils/feedgenerator.py
@@ -104,10 +104,14 @@ class FeedgeneratorTest(unittest.TestCase):
feed = feedgenerator.Rss201rev2Feed('title', '/link/', 'descr')
self.assertEqual(feed.feed['feed_url'], None)
feed_content = feed.writeString('utf-8')
- self.assertNotIn('<atom:link href=', feed_content)
+ self.assertNotIn('<atom:link', feed_content)
+ self.assertNotIn('href="/feed/"', feed_content)
+ self.assertNotIn('rel="self"', feed_content)
def test_feed_with_feed_url_gets_rendered_with_atom_link(self):
feed = feedgenerator.Rss201rev2Feed('title', '/link/', 'descr', feed_url='/feed/')
self.assertEqual(feed.feed['feed_url'], '/feed/')
feed_content = feed.writeString('utf-8')
- self.assertIn('<atom:link href="/feed/" rel="self"></atom:link>', feed_content)
+ self.assertIn('<atom:link', feed_content)
+ self.assertIn('href="/feed/"', feed_content)
+ self.assertIn('rel="self"', feed_content)
diff --git a/tests/regressiontests/utils/html.py b/tests/regressiontests/utils/html.py
index 6a93dff85e..a0226c4765 100644
--- a/tests/regressiontests/utils/html.py
+++ b/tests/regressiontests/utils/html.py
@@ -65,6 +65,9 @@ class TestUtilsHtml(unittest.TestCase):
('<f', '<f'),
('</fe', '</fe'),
('<x>b<y>', 'b'),
+ ('a<p onclick="alert(\'<test>\')">b</p>c', 'abc'),
+ ('a<p a >b</p>c', 'abc'),
+ ('d<a:b c:d>e</p>f', 'def'),
)
for value, output in items:
self.check_output(f, value, output)
diff --git a/tests/regressiontests/utils/module_loading.py b/tests/regressiontests/utils/module_loading.py
index dffb51966c..3fc92b0862 100644
--- a/tests/regressiontests/utils/module_loading.py
+++ b/tests/regressiontests/utils/module_loading.py
@@ -6,6 +6,7 @@ from zipimport import zipimporter
from django.utils import unittest
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
+from django.utils._os import upath
class DefaultLoader(unittest.TestCase):
@@ -50,7 +51,7 @@ class DefaultLoader(unittest.TestCase):
class EggLoader(unittest.TestCase):
def setUp(self):
self.old_path = sys.path[:]
- self.egg_dir = '%s/eggs' % os.path.dirname(__file__)
+ self.egg_dir = '%s/eggs' % os.path.dirname(upath(__file__))
def tearDown(self):
sys.path = self.old_path
diff --git a/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.mo b/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.mo
new file mode 100644
index 0000000000..34ba691029
--- /dev/null
+++ b/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.mo
Binary files differ
diff --git a/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.po b/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.po
new file mode 100644
index 0000000000..88cc35e88f
--- /dev/null
+++ b/tests/regressiontests/views/locale/de/LC_MESSAGES/djangojs.po
@@ -0,0 +1,40 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django tests\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-02-14 17:33+0100\n"
+"PO-Revision-Date: 2011-01-21 21:37-0300\n"
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
+"Language-Team: de <de@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+#: models.py:7
+msgctxt "month name"
+msgid "May"
+msgstr "Mai"
+
+#: models.py:9
+msgctxt "verb"
+msgid "May"
+msgstr "Kann"
+
+#: models.py:11
+msgid "%s item"
+msgid_plural "%s items"
+msgstr[0] "%s Element"
+msgstr[1] "%s Elemente"
+
+#: models.py:11
+msgctxt "search"
+msgid "%s result"
+msgid_plural "%s results"
+msgstr[0] "%s Resultat"
+msgstr[1] "%s Resultate"
diff --git a/tests/regressiontests/views/templates/jsi18n.html b/tests/regressiontests/views/templates/jsi18n.html
new file mode 100644
index 0000000000..d6093c8ef4
--- /dev/null
+++ b/tests/regressiontests/views/templates/jsi18n.html
@@ -0,0 +1,44 @@
+<html>
+<head>
+ <script type="text/javascript" src="/jsi18n_admin/"></script>
+</head>
+
+<body>
+ <p id="gettext">
+ <script type="text/javascript">
+ document.write(gettext("Remove"));
+ </script>
+ </p>
+
+ <p id="ngettext_sing">
+ <script type="text/javascript">
+ document.write(interpolate(ngettext("%s item", "%s items", 1), [1]));
+ </script>
+ </p>
+
+ <p id="ngettext_plur">
+ <script type="text/javascript">
+ document.write(interpolate(ngettext("%s item", "%s items", 455), [455]));
+ </script>
+ </p>
+
+ <p id="pgettext">
+ <script type="text/javascript">
+ document.write(pgettext("verb", "May"));
+ </script>
+ </p>
+
+ <p id="npgettext_sing">
+ <script type="text/javascript">
+ document.write(interpolate(npgettext("search", "%s result", "%s results", 1), [1]));
+ </script>
+ </p>
+
+ <p id="npgettext_plur">
+ <script type="text/javascript">
+ document.write(interpolate(npgettext("search", "%s result", "%s results", 455), [455]));
+ </script>
+ </p>
+
+</body>
+</html>
diff --git a/tests/regressiontests/views/tests/__init__.py b/tests/regressiontests/views/tests/__init__.py
index 12d0c59014..17f3f4562d 100644
--- a/tests/regressiontests/views/tests/__init__.py
+++ b/tests/regressiontests/views/tests/__init__.py
@@ -4,7 +4,7 @@ from .debug import (DebugViewTests, ExceptionReporterTests,
ExceptionReporterTests, PlainTextReportTests, ExceptionReporterFilterTests,
AjaxResponseExceptionReporterFilter)
from .defaults import DefaultsTests
-from .i18n import JsI18NTests, I18NTests, JsI18NTestsMultiPackage
+from .i18n import JsI18NTests, I18NTests, JsI18NTestsMultiPackage, JavascriptI18nTests
from .shortcuts import ShortcutTests
from .specials import URLHandling
from .static import StaticHelperTest, StaticUtilsTests, StaticTests
diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py
index e616d184b8..4fdaad5010 100644
--- a/tests/regressiontests/views/tests/debug.py
+++ b/tests/regressiontests/views/tests/debug.py
@@ -14,6 +14,7 @@ from django.core.urlresolvers import reverse
from django.test import TestCase, RequestFactory
from django.test.utils import (override_settings, setup_test_template_loader,
restore_template_loaders)
+from django.utils.encoding import force_text
from django.views.debug import ExceptionReporter
from .. import BrokenException, except_args
@@ -306,15 +307,16 @@ class ExceptionReportTestMixin(object):
self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
# Frames vars are never shown in plain text email reports.
- self.assertNotIn('cooked_eggs', email.body)
- self.assertNotIn('scrambled', email.body)
- self.assertNotIn('sauce', email.body)
- self.assertNotIn('worcestershire', email.body)
+ body = force_text(email.body)
+ self.assertNotIn('cooked_eggs', body)
+ self.assertNotIn('scrambled', body)
+ self.assertNotIn('sauce', body)
+ self.assertNotIn('worcestershire', body)
if check_for_POST_params:
for k, v in self.breakfast_data.items():
# All POST parameters are shown.
- self.assertIn(k, email.body)
- self.assertIn(v, email.body)
+ self.assertIn(k, body)
+ self.assertIn(v, body)
def verify_safe_email(self, view, check_for_POST_params=True):
"""
@@ -327,20 +329,21 @@ class ExceptionReportTestMixin(object):
self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
# Frames vars are never shown in plain text email reports.
- self.assertNotIn('cooked_eggs', email.body)
- self.assertNotIn('scrambled', email.body)
- self.assertNotIn('sauce', email.body)
- self.assertNotIn('worcestershire', email.body)
+ body = force_text(email.body)
+ self.assertNotIn('cooked_eggs', body)
+ self.assertNotIn('scrambled', body)
+ self.assertNotIn('sauce', body)
+ self.assertNotIn('worcestershire', body)
if check_for_POST_params:
for k, v in self.breakfast_data.items():
# All POST parameters' names are shown.
- self.assertIn(k, email.body)
+ self.assertIn(k, body)
# Non-sensitive POST parameters' values are shown.
- self.assertIn('baked-beans-value', email.body)
- self.assertIn('hash-brown-value', email.body)
+ self.assertIn('baked-beans-value', body)
+ self.assertIn('hash-brown-value', body)
# Sensitive POST parameters' values are not shown.
- self.assertNotIn('sausage-value', email.body)
- self.assertNotIn('bacon-value', email.body)
+ self.assertNotIn('sausage-value', body)
+ self.assertNotIn('bacon-value', body)
def verify_paranoid_email(self, view):
"""
@@ -353,15 +356,16 @@ class ExceptionReportTestMixin(object):
self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
# Frames vars are never shown in plain text email reports.
- self.assertNotIn('cooked_eggs', email.body)
- self.assertNotIn('scrambled', email.body)
- self.assertNotIn('sauce', email.body)
- self.assertNotIn('worcestershire', email.body)
+ body = force_text(email.body)
+ self.assertNotIn('cooked_eggs', body)
+ self.assertNotIn('scrambled', body)
+ self.assertNotIn('sauce', body)
+ self.assertNotIn('worcestershire', body)
for k, v in self.breakfast_data.items():
# All POST parameters' names are shown.
- self.assertIn(k, email.body)
+ self.assertIn(k, body)
# No POST parameters' values are shown.
- self.assertNotIn(v, email.body)
+ self.assertNotIn(v, body)
class ExceptionReporterFilterTests(TestCase, ExceptionReportTestMixin):
diff --git a/tests/regressiontests/views/tests/i18n.py b/tests/regressiontests/views/tests/i18n.py
index 601df6d512..b1dc8808a1 100644
--- a/tests/regressiontests/views/tests/i18n.py
+++ b/tests/regressiontests/views/tests/i18n.py
@@ -6,11 +6,18 @@ from os import path
from django.conf import settings
from django.core.urlresolvers import reverse
-from django.test import TestCase
-from django.utils import six
-from django.utils.translation import override, get_language
+from django.test import LiveServerTestCase, TestCase
+from django.test.utils import override_settings
+from django.utils import six, unittest
+from django.utils._os import upath
+from django.utils.translation import override
from django.utils.text import javascript_quote
+try:
+ from selenium.webdriver.firefox import webdriver as firefox
+except ImportError:
+ firefox = None
+
from ..urls import locale_dir
@@ -18,13 +25,28 @@ class I18NTests(TestCase):
""" Tests django views in django/views/i18n.py """
def test_setlang(self):
- """The set_language view can be used to change the session language"""
+ """
+ The set_language view can be used to change the session language.
+
+ The user is redirected to the 'next' argument if provided.
+ """
for lang_code, lang_name in settings.LANGUAGES:
post_data = dict(language=lang_code, next='/views/')
response = self.client.post('/views/i18n/setlang/', data=post_data)
self.assertRedirects(response, 'http://testserver/views/')
self.assertEqual(self.client.session['django_language'], lang_code)
+ def test_setlang_unsafe_next(self):
+ """
+ The set_language view only redirects to the 'next' argument if it is
+ "safe".
+ """
+ lang_code, lang_name = settings.LANGUAGES[0]
+ post_data = dict(language=lang_code, next='//unsafe/redirection/')
+ response = self.client.post('/views/i18n/setlang/', data=post_data)
+ self.assertEqual(response['Location'], 'http://testserver/')
+ self.assertEqual(self.client.session['django_language'], lang_code)
+
def test_setlang_reversal(self):
self.assertEqual(reverse('set_language'), '/views/i18n/setlang/')
@@ -146,9 +168,49 @@ class JsI18NTestsMultiPackage(TestCase):
def testI18NWithLocalePaths(self):
extended_locale_paths = settings.LOCALE_PATHS + (
path.join(path.dirname(
- path.dirname(path.abspath(__file__))), 'app3', 'locale'),)
+ path.dirname(path.abspath(upath(__file__)))), 'app3', 'locale'),)
with self.settings(LANGUAGE_CODE='es-ar', LOCALE_PATHS=extended_locale_paths):
with override('es-ar'):
response = self.client.get('/views/jsi18n/')
self.assertContains(response,
javascript_quote('este texto de app3 debe ser traducido'))
+
+
+@unittest.skipUnless(firefox, 'Selenium not installed')
+class JavascriptI18nTests(LiveServerTestCase):
+ urls = 'regressiontests.views.urls'
+
+ @classmethod
+ def setUpClass(cls):
+ cls.selenium = firefox.WebDriver()
+ super(JavascriptI18nTests, cls).setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.selenium.quit()
+ super(JavascriptI18nTests, cls).tearDownClass()
+
+ @override_settings(LANGUAGE_CODE='de')
+ def test_javascript_gettext(self):
+ extended_apps = list(settings.INSTALLED_APPS) + ['regressiontests.views']
+ with self.settings(INSTALLED_APPS=extended_apps):
+ self.selenium.get('%s%s' % (self.live_server_url, '/jsi18n_template/'))
+
+ elem = self.selenium.find_element_by_id("gettext")
+ self.assertEqual(elem.text, "Entfernen")
+ elem = self.selenium.find_element_by_id("ngettext_sing")
+ self.assertEqual(elem.text, "1 Element")
+ elem = self.selenium.find_element_by_id("ngettext_plur")
+ self.assertEqual(elem.text, "455 Elemente")
+ elem = self.selenium.find_element_by_id("pgettext")
+ self.assertEqual(elem.text, "Kann")
+ elem = self.selenium.find_element_by_id("npgettext_sing")
+ self.assertEqual(elem.text, "1 Resultat")
+ elem = self.selenium.find_element_by_id("npgettext_plur")
+ self.assertEqual(elem.text, "455 Resultate")
+
+ def test_escaping(self):
+ extended_apps = list(settings.INSTALLED_APPS) + ['regressiontests.views']
+ with self.settings(INSTALLED_APPS=extended_apps):
+ response = self.client.get('%s%s' % (self.live_server_url, '/jsi18n_admin/'))
+ self.assertContains(response, '\\x04')
diff --git a/tests/regressiontests/views/urls.py b/tests/regressiontests/views/urls.py
index 90d2382f71..2c06557ae9 100644
--- a/tests/regressiontests/views/urls.py
+++ b/tests/regressiontests/views/urls.py
@@ -4,11 +4,12 @@ from __future__ import absolute_import
from os import path
from django.conf.urls import patterns, url, include
+from django.utils._os import upath
from . import views
-base_dir = path.dirname(path.abspath(__file__))
+base_dir = path.dirname(path.abspath(upath(__file__)))
media_dir = path.join(base_dir, 'media')
locale_dir = path.join(base_dir, 'locale')
@@ -32,6 +33,11 @@ js_info_dict_multi_packages2 = {
'packages': ('regressiontests.views.app3', 'regressiontests.views.app4'),
}
+js_info_dict_admin = {
+ 'domain': 'djangojs',
+ 'packages': ('django.contrib.admin', 'regressiontests.views'),
+}
+
urlpatterns = patterns('',
(r'^$', views.index_page),
@@ -51,6 +57,8 @@ urlpatterns = patterns('',
(r'^jsi18n_english_translation/$', 'django.views.i18n.javascript_catalog', js_info_dict_english_translation),
(r'^jsi18n_multi_packages1/$', 'django.views.i18n.javascript_catalog', js_info_dict_multi_packages1),
(r'^jsi18n_multi_packages2/$', 'django.views.i18n.javascript_catalog', js_info_dict_multi_packages2),
+ (r'^jsi18n_admin/$', 'django.views.i18n.javascript_catalog', js_info_dict_admin),
+ (r'^jsi18n_template/$', views.jsi18n),
# Static views
(r'^site_media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': media_dir}),
diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py
index 2836d1bdde..ed9d61144a 100644
--- a/tests/regressiontests/views/views.py
+++ b/tests/regressiontests/views/views.py
@@ -51,6 +51,9 @@ def template_exception(request, n):
return render_to_response('debug/template_exception.html',
{'arg': except_args[int(n)]})
+def jsi18n(request):
+ return render_to_response('jsi18n.html')
+
# Some views to exercise the shortcuts
def render_to_response_view(request):
diff --git a/tests/runtests.py b/tests/runtests.py
index a81fee6858..8c56e273b5 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -7,6 +7,7 @@ import tempfile
import warnings
from django import contrib
+from django.utils._os import upath
from django.utils import six
# databrowse is deprecated, but we still want to run its tests
@@ -19,8 +20,8 @@ REGRESSION_TESTS_DIR_NAME = 'regressiontests'
TEST_TEMPLATE_DIR = 'templates'
-RUNTESTS_DIR = os.path.dirname(__file__)
-CONTRIB_DIR = os.path.dirname(contrib.__file__)
+RUNTESTS_DIR = os.path.dirname(upath(__file__))
+CONTRIB_DIR = os.path.dirname(upath(contrib.__file__))
MODEL_TEST_DIR = os.path.join(RUNTESTS_DIR, MODEL_TESTS_DIR_NAME)
REGRESSION_TEST_DIR = os.path.join(RUNTESTS_DIR, REGRESSION_TESTS_DIR_NAME)
TEMP_DIR = tempfile.mkdtemp(prefix='django_')
@@ -192,7 +193,7 @@ def bisect_tests(bisection_label, options, test_labels):
pass
subprocess_args = [
- sys.executable, __file__, '--settings=%s' % options.settings]
+ sys.executable, upath(__file__), '--settings=%s' % options.settings]
if options.failfast:
subprocess_args.append('--failfast')
if options.verbosity:
@@ -253,7 +254,7 @@ def paired_tests(paired_test, options, test_labels):
pass
subprocess_args = [
- sys.executable, __file__, '--settings=%s' % options.settings]
+ sys.executable, upath(__file__), '--settings=%s' % options.settings]
if options.failfast:
subprocess_args.append('--failfast')
if options.verbosity:
@@ -277,7 +278,7 @@ if __name__ == "__main__":
usage = "%prog [options] [module module module ...]"
parser = OptionParser(usage=usage)
parser.add_option(
- '-v','--verbosity', action='store', dest='verbosity', default='1',
+ '-v', '--verbosity', action='store', dest='verbosity', default='1',
type='choice', choices=['0', '1', '2', '3'],
help='Verbosity level; 0=minimal output, 1=normal output, 2=all '
'output')