summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHonza Král <honza.kral@gmail.com>2009-11-07 17:09:09 +0000
committerHonza Král <honza.kral@gmail.com>2009-11-07 17:09:09 +0000
commit30ea350dabad28b0e524feabce434d446e013d6f (patch)
tree880b6b01d6cbf9423943025f77d8b4ad0b460e83
parentdfe495fbe8e360ee3b3cd8b29e55ee19d86fc9d2 (diff)
[soc2009/model-validation] Merged to trunk at r11724
git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/model-validation@11725 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--AUTHORS7
-rw-r--r--INSTALL10
-rw-r--r--django/conf/__init__.py8
-rw-r--r--django/conf/global_settings.py19
-rw-r--r--django/conf/locale/pl/LC_MESSAGES/django.mobin65771 -> 66081 bytes
-rw-r--r--django/conf/locale/pl/LC_MESSAGES/django.po223
-rw-r--r--django/conf/project_template/settings.py1
-rw-r--r--django/contrib/admin/media/css/changelists.css2
-rw-r--r--django/contrib/admin/options.py14
-rw-r--r--django/contrib/admin/sites.py7
-rw-r--r--django/contrib/admin/templates/admin/auth/user/change_password.html2
-rw-r--r--django/contrib/admin/templates/admin/change_form.html2
-rw-r--r--django/contrib/admin/templates/admin/change_list.html2
-rw-r--r--django/contrib/admin/templates/admin/delete_confirmation.html2
-rw-r--r--django/contrib/admin/templates/admin/delete_selected_confirmation.html2
-rw-r--r--django/contrib/admin/templates/admin/login.html2
-rw-r--r--django/contrib/admin/templates/admin/template_validator.html2
-rw-r--r--django/contrib/admin/templates/registration/password_change_form.html2
-rw-r--r--django/contrib/admin/templates/registration/password_reset_confirm.html2
-rw-r--r--django/contrib/admin/templates/registration/password_reset_form.html2
-rw-r--r--django/contrib/admin/templatetags/admin_list.py5
-rw-r--r--django/contrib/admin/validation.py5
-rw-r--r--django/contrib/auth/tests/remote_user.py10
-rw-r--r--django/contrib/auth/views.py25
-rw-r--r--django/contrib/comments/admin.py41
-rw-r--r--django/contrib/comments/templates/comments/approve.html2
-rw-r--r--django/contrib/comments/templates/comments/delete.html2
-rw-r--r--django/contrib/comments/templates/comments/flag.html2
-rw-r--r--django/contrib/comments/templates/comments/form.html2
-rw-r--r--django/contrib/comments/templates/comments/moderation_queue.html75
-rw-r--r--django/contrib/comments/templates/comments/preview.html2
-rw-r--r--django/contrib/comments/urls.py1
-rw-r--r--django/contrib/comments/views/comments.py5
-rw-r--r--django/contrib/comments/views/moderation.py165
-rw-r--r--django/contrib/contenttypes/generic.py2
-rw-r--r--django/contrib/csrf/middleware.py165
-rw-r--r--django/contrib/csrf/tests.py144
-rw-r--r--django/contrib/formtools/templates/formtools/form.html2
-rw-r--r--django/contrib/formtools/templates/formtools/preview.html4
-rw-r--r--django/contrib/formtools/tests.py9
-rw-r--r--django/contrib/formtools/wizard.py2
-rw-r--r--django/contrib/gis/gdal/geometries.py61
-rw-r--r--django/contrib/gis/gdal/prototypes/geom.py3
-rw-r--r--django/contrib/gis/gdal/tests/test_geom.py12
-rw-r--r--django/contrib/gis/tests/geoapp/test_regress.py1
-rw-r--r--django/contrib/gis/tests/layermap/models.py14
-rw-r--r--django/contrib/gis/tests/layermap/tests.py22
-rw-r--r--django/contrib/gis/utils/layermapping.py20
-rw-r--r--django/core/context_processors.py49
-rw-r--r--django/core/files/storage.py4
-rw-r--r--django/core/mail/__init__.py110
-rw-r--r--django/core/mail/backends/__init__.py1
-rw-r--r--django/core/mail/backends/base.py39
-rw-r--r--django/core/mail/backends/console.py37
-rw-r--r--django/core/mail/backends/dummy.py9
-rw-r--r--django/core/mail/backends/filebased.py59
-rw-r--r--django/core/mail/backends/locmem.py24
-rw-r--r--django/core/mail/backends/smtp.py106
-rw-r--r--django/core/mail/message.py (renamed from django/core/mail.py)183
-rw-r--r--django/core/mail/utils.py19
-rw-r--r--django/core/management/commands/syncdb.py20
-rw-r--r--django/core/management/sql.py15
-rw-r--r--django/core/management/validation.py151
-rw-r--r--django/core/serializers/python.py2
-rw-r--r--django/core/serializers/xml_serializer.py3
-rw-r--r--django/db/models/base.py59
-rw-r--r--django/db/models/fields/related.py296
-rw-r--r--django/db/models/loading.py12
-rw-r--r--django/db/models/manager.py4
-rw-r--r--django/db/models/options.py4
-rw-r--r--django/db/models/query.py18
-rw-r--r--django/db/models/sql/query.py16
-rw-r--r--django/forms/models.py159
-rw-r--r--django/middleware/csrf.py265
-rw-r--r--django/template/__init__.py10
-rw-r--r--django/template/context.py10
-rw-r--r--django/template/defaultfilters.py2
-rw-r--r--django/template/defaulttags.py21
-rw-r--r--django/test/client.py13
-rw-r--r--django/test/utils.py30
-rw-r--r--django/utils/decorators.py13
-rw-r--r--django/utils/functional.py73
-rw-r--r--django/views/csrf.py69
-rw-r--r--django/views/decorators/csrf.py47
-rw-r--r--docs/faq/install.txt45
-rw-r--r--docs/internals/committers.txt13
-rw-r--r--docs/internals/deprecation.txt15
-rw-r--r--docs/internals/release-process.txt2
-rw-r--r--docs/intro/install.txt2
-rw-r--r--docs/intro/tutorial01.txt1
-rw-r--r--docs/intro/tutorial02.txt10
-rw-r--r--docs/intro/tutorial03.txt22
-rw-r--r--docs/intro/tutorial04.txt31
-rw-r--r--docs/ref/contrib/admin/index.txt70
-rw-r--r--docs/ref/contrib/comments/index.txt7
-rw-r--r--docs/ref/contrib/csrf.txt448
-rw-r--r--docs/ref/contrib/formtools/form-wizard.txt2
-rw-r--r--docs/ref/middleware.txt4
-rw-r--r--docs/ref/models/querysets.txt11
-rw-r--r--docs/ref/settings.txt69
-rw-r--r--docs/ref/templates/api.txt15
-rw-r--r--docs/ref/templates/builtins.txt10
-rw-r--r--docs/releases/1.2-alpha.txt52
-rw-r--r--docs/topics/auth.txt36
-rw-r--r--docs/topics/cache.txt6
-rw-r--r--docs/topics/conditional-view-processing.txt7
-rw-r--r--docs/topics/email.txt405
-rw-r--r--docs/topics/http/middleware.txt1
-rw-r--r--docs/topics/install.txt10
-rw-r--r--docs/topics/testing.txt12
-rw-r--r--extras/csrf_migration_helper.py369
-rw-r--r--tests/modeltests/invalid_models/models.py1
-rw-r--r--tests/modeltests/lookup/models.py8
-rw-r--r--tests/modeltests/m2m_through/models.py10
-rw-r--r--tests/modeltests/model_package/__init__.py1
-rw-r--r--tests/modeltests/model_package/models/__init__.py3
-rw-r--r--tests/modeltests/model_package/models/article.py10
-rw-r--r--tests/modeltests/model_package/models/publication.py7
-rw-r--r--tests/modeltests/model_package/tests.py34
-rw-r--r--tests/regressiontests/admin_validation/models.py39
-rw-r--r--tests/regressiontests/admin_views/tests.py15
-rw-r--r--tests/regressiontests/cache/models.py11
-rw-r--r--tests/regressiontests/cache/tests.py42
-rw-r--r--tests/regressiontests/comment_tests/tests/moderation_view_tests.py47
-rw-r--r--tests/regressiontests/comment_tests/urls_admin.py13
-rw-r--r--tests/regressiontests/context_processors/fixtures/context-processors-users.xml17
-rw-r--r--tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html1
-rw-r--r--tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html1
-rw-r--r--tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html1
-rw-r--r--tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html1
-rw-r--r--tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html1
-rw-r--r--tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html4
-rw-r--r--tests/regressiontests/context_processors/tests.py76
-rw-r--r--tests/regressiontests/context_processors/urls.py6
-rw-r--r--tests/regressiontests/context_processors/views.py29
-rw-r--r--tests/regressiontests/csrf_tests/__init__.py0
-rw-r--r--tests/regressiontests/csrf_tests/models.py (renamed from django/contrib/csrf/models.py)0
-rw-r--r--tests/regressiontests/csrf_tests/tests.py324
-rw-r--r--tests/regressiontests/dateformat/tests.py166
-rw-r--r--tests/regressiontests/m2m_through_regress/models.py8
-rw-r--r--tests/regressiontests/mail/custombackend.py15
-rw-r--r--tests/regressiontests/mail/tests.py234
-rw-r--r--tests/regressiontests/model_formsets_regress/tests.py10
-rw-r--r--tests/regressiontests/model_inheritance_regress/models.py66
-rw-r--r--tests/regressiontests/serializers_regress/models.py8
-rw-r--r--tests/regressiontests/signals_regress/__init__.py0
-rw-r--r--tests/regressiontests/signals_regress/models.py89
-rw-r--r--tests/regressiontests/test_client_regress/models.py17
-rw-r--r--tests/regressiontests/utils/dateformat.py73
-rw-r--r--tests/regressiontests/utils/tests.py78
-rw-r--r--tests/regressiontests/views/tests/generic/date_based.py4
151 files changed, 4643 insertions, 1568 deletions
diff --git a/AUTHORS b/AUTHORS
index 2135afcc7b..50ca0af541 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -16,6 +16,7 @@ The PRIMARY AUTHORS are (and/or have been):
* Brian Rosner
* Justin Bronn
* Karen Tracey
+ * Jannis Leidel
More information on the main contributors to Django can be found in
docs/internals/committers.txt.
@@ -26,6 +27,7 @@ answer newbie questions, and generally made Django that much better:
ajs <adi@sieker.info>
alang@bright-green.com
+ Andi Albrecht <albrecht.andi@gmail.com>
Marty Alchin <gulopine@gamemusic.org>
Ahmad Alhashemi <trans@ahmadh.com>
Daniel Alves Barbosa de Oliveira Vaz <danielvaz@gmail.com>
@@ -268,7 +270,6 @@ answer newbie questions, and generally made Django that much better:
lcordier@point45.com
Jeong-Min Lee <falsetru@gmail.com>
Tai Lee <real.human@mrmachine.net>
- Jannis Leidel <jl@websushi.org>
Christopher Lenz <http://www.cmlenz.net/>
lerouxb@gmail.com
Piotr Lewandowski <piotr.lewandowski@gmail.com>
@@ -423,7 +424,7 @@ answer newbie questions, and generally made Django that much better:
Travis Terry <tdterry7@gmail.com>
thebjorn <bp@datakortet.no>
Zach Thompson <zthompson47@gmail.com>
- Michael Thornhill
+ Michael Thornhill <michael.thornhill@gmail.com>
Deepak Thukral <deep.thukral@gmail.com>
tibimicu@gmx.net
tobias@neuyork.de
@@ -471,6 +472,8 @@ answer newbie questions, and generally made Django that much better:
Gasper Zejn <zejn@kiberpipa.org>
Jarek Zgoda <jarek.zgoda@gmail.com>
Cheng Zhang
+ Glenn Maynard <glenn@zewt.org>
+ bthomas
A big THANK YOU goes to:
diff --git a/INSTALL b/INSTALL
index 9b79ec016b..644c524bbb 100644
--- a/INSTALL
+++ b/INSTALL
@@ -1,22 +1,16 @@
Thanks for downloading Django.
-To install it, make sure you have Python 2.3 or greater installed. Then run
+To install it, make sure you have Python 2.4 or greater installed. Then run
this command from the command prompt:
python setup.py install
-Note this requires a working Internet connection if you don't already have the
-Python utility "setuptools" installed.
-
AS AN ALTERNATIVE, you can just copy the entire "django" directory to Python's
site-packages directory, which is located wherever your Python installation
lives. Some places you might check are:
+ /usr/lib/python2.5/site-packages (Unix, Python 2.5)
/usr/lib/python2.4/site-packages (Unix, Python 2.4)
- /usr/lib/python2.3/site-packages (Unix, Python 2.3)
C:\\PYTHON\site-packages (Windows)
-This second solution does not require a working Internet connection; it
-bypasses "setuptools" entirely.
-
For more detailed instructions, see docs/intro/install.txt.
diff --git a/django/conf/__init__.py b/django/conf/__init__.py
index 7bc7ae9508..7fbfd26534 100644
--- a/django/conf/__init__.py
+++ b/django/conf/__init__.py
@@ -108,9 +108,6 @@ class Settings(object):
os.environ['TZ'] = self.TIME_ZONE
time.tzset()
- def get_all_members(self):
- return dir(self)
-
class UserSettingsHolder(object):
"""
Holder for user configured settings.
@@ -129,8 +126,11 @@ class UserSettingsHolder(object):
def __getattr__(self, name):
return getattr(self.default_settings, name)
- def get_all_members(self):
+ def __dir__(self):
return dir(self) + dir(self.default_settings)
+ # For Python < 2.6:
+ __members__ = property(lambda self: self.__dir__())
+
settings = LazySettings()
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 99fc72e468..70d9c1e259 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -131,6 +131,12 @@ DATABASE_HOST = '' # Set to empty string for localhost. Not used wit
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
DATABASE_OPTIONS = {} # Set to empty dictionary for default.
+# The email backend to use. For possible shortcuts see django.core.mail.
+# The default is to use the SMTP backend.
+# Third-party backends can be specified by providing a Python path
+# to a module that defines an EmailBackend class.
+EMAIL_BACKEND = 'django.core.mail.backends.smtp'
+
# Host for sending e-mail.
EMAIL_HOST = 'localhost'
@@ -300,6 +306,7 @@ DEFAULT_INDEX_TABLESPACE = ''
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
# 'django.middleware.http.ConditionalGetMiddleware',
# 'django.middleware.gzip.GZipMiddleware',
@@ -374,6 +381,18 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
# The number of days a password reset link is valid for
PASSWORD_RESET_TIMEOUT_DAYS = 3
+########
+# CSRF #
+########
+
+# Dotted path to callable to be used as view when a request is
+# rejected by the CSRF middleware.
+CSRF_FAILURE_VIEW = 'django.views.csrf.csrf_failure'
+
+# Name and domain for CSRF cookie.
+CSRF_COOKIE_NAME = 'csrftoken'
+CSRF_COOKIE_DOMAIN = None
+
###########
# TESTING #
###########
diff --git a/django/conf/locale/pl/LC_MESSAGES/django.mo b/django/conf/locale/pl/LC_MESSAGES/django.mo
index 309db85baf..0106a8c904 100644
--- a/django/conf/locale/pl/LC_MESSAGES/django.mo
+++ b/django/conf/locale/pl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/django/conf/locale/pl/LC_MESSAGES/django.po b/django/conf/locale/pl/LC_MESSAGES/django.po
index ba985d01e9..28f12561ee 100644
--- a/django/conf/locale/pl/LC_MESSAGES/django.po
+++ b/django/conf/locale/pl/LC_MESSAGES/django.po
@@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2009-07-17 21:59+0200\n"
+"POT-Creation-Date: 2009-10-25 20:56+0100\n"
"PO-Revision-Date: 2008-02-25 15:53+0100\n"
"Last-Translator: Jarek Zgoda <jarek.zgoda@gmail.com>\n"
"MIME-Version: 1.0\n"
@@ -266,15 +266,15 @@ msgstr "Ten miesiąc"
msgid "This year"
msgstr "Ten rok"
-#: contrib/admin/filterspecs.py:147 forms/widgets.py:434
+#: contrib/admin/filterspecs.py:147 forms/widgets.py:435
msgid "Yes"
msgstr "Tak"
-#: contrib/admin/filterspecs.py:147 forms/widgets.py:434
+#: contrib/admin/filterspecs.py:147 forms/widgets.py:435
msgid "No"
msgstr "Nie"
-#: contrib/admin/filterspecs.py:154 forms/widgets.py:434
+#: contrib/admin/filterspecs.py:154 forms/widgets.py:435
msgid "Unknown"
msgstr "Nieznany"
@@ -320,8 +320,8 @@ msgid "Changed %s."
msgstr "Zmieniono %s"
#: contrib/admin/options.py:519 contrib/admin/options.py:529
-#: contrib/comments/templates/comments/preview.html:16 forms/models.py:388
-#: forms/models.py:600
+#: contrib/comments/templates/comments/preview.html:16 forms/models.py:384
+#: forms/models.py:596
msgid "and"
msgstr "i"
@@ -417,11 +417,11 @@ msgstr ""
"Proszę wpisać poprawną nazwę użytkownika i hasło. Uwaga: wielkość liter ma "
"znaczenie."
-#: contrib/admin/sites.py:285 contrib/admin/views/decorators.py:40
+#: contrib/admin/sites.py:288 contrib/admin/views/decorators.py:40
msgid "Please log in again, because your session has expired."
msgstr "Twoja sesja wygasła, zaloguj się ponownie."
-#: contrib/admin/sites.py:292 contrib/admin/views/decorators.py:47
+#: contrib/admin/sites.py:295 contrib/admin/views/decorators.py:47
msgid ""
"Looks like your browser isn't configured to accept cookies. Please enable "
"cookies, reload this page, and try again."
@@ -429,27 +429,27 @@ msgstr ""
"Twoja przeglądarka nie chce akceptować ciasteczek. Zmień jej ustawienia i "
"spróbuj ponownie."
-#: contrib/admin/sites.py:308 contrib/admin/sites.py:314
+#: contrib/admin/sites.py:311 contrib/admin/sites.py:317
#: contrib/admin/views/decorators.py:66
msgid "Usernames cannot contain the '@' character."
msgstr "Nazwy użytkowników nie mogą zawierać znaku '@'."
-#: contrib/admin/sites.py:311 contrib/admin/views/decorators.py:62
+#: contrib/admin/sites.py:314 contrib/admin/views/decorators.py:62
#, python-format
msgid "Your e-mail address is not your username. Try '%s' instead."
msgstr "Podany adres e-mail nie jest Twoją nazwą użytkownika. Spróbuj '%s'."
-#: contrib/admin/sites.py:367
+#: contrib/admin/sites.py:370
msgid "Site administration"
msgstr "Administracja stroną"
-#: contrib/admin/sites.py:381 contrib/admin/templates/admin/login.html:26
+#: contrib/admin/sites.py:384 contrib/admin/templates/admin/login.html:26
#: contrib/admin/templates/registration/password_reset_complete.html:14
#: contrib/admin/views/decorators.py:20
msgid "Log in"
msgstr "Zaloguj się"
-#: contrib/admin/sites.py:426
+#: contrib/admin/sites.py:429
#, python-format
msgid "%s administration"
msgstr "%s - administracja"
@@ -464,27 +464,27 @@ msgstr "Jedno lub więcej %(fieldname)s w %(name)s: %(obj)s"
msgid "One or more %(fieldname)s in %(name)s:"
msgstr "Jedno lub więcej %(fieldname)s w %(name)s:"
-#: contrib/admin/widgets.py:71
+#: contrib/admin/widgets.py:72
msgid "Date:"
msgstr "Data:"
-#: contrib/admin/widgets.py:71
+#: contrib/admin/widgets.py:72
msgid "Time:"
msgstr "Czas:"
-#: contrib/admin/widgets.py:95
+#: contrib/admin/widgets.py:96
msgid "Currently:"
msgstr "Teraz:"
-#: contrib/admin/widgets.py:95
+#: contrib/admin/widgets.py:96
msgid "Change:"
msgstr "Zmień:"
-#: contrib/admin/widgets.py:124
+#: contrib/admin/widgets.py:125
msgid "Lookup"
msgstr "Szukaj"
-#: contrib/admin/widgets.py:235
+#: contrib/admin/widgets.py:237
msgid "Add Another"
msgstr "Dodaj kolejny"
@@ -598,7 +598,7 @@ msgstr "Historia"
#: contrib/admin/templates/admin/change_form.html:28
#: contrib/admin/templates/admin/edit_inline/stacked.html:13
-#: contrib/admin/templates/admin/edit_inline/tabular.html:27
+#: contrib/admin/templates/admin/edit_inline/tabular.html:28
msgid "View on site"
msgstr "Pokaż na stronie"
@@ -668,10 +668,10 @@ msgstr ""
#, python-format
msgid ""
"Are you sure you want to delete the selected %(object_name)s objects? All of "
-"the following objects and it's related items will be deleted:"
+"the following objects and their related items will be deleted:"
msgstr ""
-"Czy chcesz skasować %(object_name)s? Następujące obiekty i zależne od nich "
-"zostaną skasowane:"
+"Czy chcesz skasować wybrane %(object_name)s? Następujące obiekty i zależne od "
+"nich zostaną skasowane:"
#: contrib/admin/templates/admin/filter.html:2
#, python-format
@@ -734,7 +734,6 @@ msgid "User"
msgstr "Użytkownik"
#: contrib/admin/templates/admin/object_history.html:24
-#: contrib/comments/templates/comments/moderation_queue.html:33
msgid "Action"
msgstr "Akcja"
@@ -1125,7 +1124,6 @@ msgid "Time"
msgstr "Czas"
#: contrib/admindocs/views.py:359 contrib/comments/forms.py:95
-#: contrib/comments/templates/comments/moderation_queue.html:37
#: contrib/flatpages/admin.py:8 contrib/flatpages/models.py:7
msgid "URL"
msgstr "URL"
@@ -1428,22 +1426,54 @@ msgstr "użytkownicy"
msgid "message"
msgstr "wiadomość"
-#: contrib/auth/views.py:56
+#: contrib/auth/views.py:58
msgid "Logged out"
msgstr "Wylogowany"
-#: contrib/auth/management/commands/createsuperuser.py:23 forms/fields.py:429
+#: contrib/auth/management/commands/createsuperuser.py:23 forms/fields.py:428
msgid "Enter a valid e-mail address."
msgstr "Wprowadź poprawny adres e-mail."
-#: contrib/comments/admin.py:11
+#: contrib/comments/admin.py:12
msgid "Content"
msgstr "Zawartość"
-#: contrib/comments/admin.py:14
+#: contrib/comments/admin.py:15
msgid "Metadata"
msgstr "Metadane"
+#: contrib/comments/admin.py:39
+msgid "flagged"
+msgstr "oflagowany"
+
+#: contrib/comments/admin.py:40
+msgid "Flag selected comments"
+msgstr "Oflaguj wybrane komentarze"
+
+#: contrib/comments/admin.py:43
+msgid "approved"
+msgstr "zaakceptowany"
+
+#: contrib/comments/admin.py:44
+msgid "Approve selected comments"
+msgstr "Zaakceptuj wybrane komentarze"
+
+#: contrib/comments/admin.py:47
+msgid "removed"
+msgstr "usunięty"
+
+#: contrib/comments/admin.py:48
+msgid "Remove selected comments"
+msgstr "Usuń wybrane komentarze"
+
+#: contrib/comments/admin.py:60
+#, python-format
+msgid "1 comment was successfully %(action)s."
+msgid_plural "%(count)s comments were successfully %(action)s."
+msgstr[0] "1 komentarz został %(action)s"
+msgstr[1] "%(count)s komentarze zostały %(action)s"
+msgstr[2] "%(count)s komentarzy zostało %(action)s"
+
#: contrib/comments/feeds.py:13
#, python-format
msgid "%(site_name)s comments"
@@ -1455,7 +1485,6 @@ msgid "Latest comments on %(site_name)s"
msgstr "Ostatnie komentarze na %(site_name)s"
#: contrib/comments/forms.py:93
-#: contrib/comments/templates/comments/moderation_queue.html:34
msgid "Name"
msgstr "Nazwa"
@@ -1464,7 +1493,6 @@ msgid "Email address"
msgstr "Adres e-mail"
#: contrib/comments/forms.py:96
-#: contrib/comments/templates/comments/moderation_queue.html:35
msgid "Comment"
msgstr "Komentarz"
@@ -1592,7 +1620,6 @@ msgid "Really make this comment public?"
msgstr "Czy ten komentarz na pewno ma być publiczny?"
#: contrib/comments/templates/comments/approve.html:12
-#: contrib/comments/templates/comments/moderation_queue.html:49
msgid "Approve"
msgstr "Zaakceptuj"
@@ -1618,7 +1645,6 @@ msgid "Really remove this comment?"
msgstr "Czy na pewno usunąć ten komentarz?"
#: contrib/comments/templates/comments/delete.html:12
-#: contrib/comments/templates/comments/moderation_queue.html:53
msgid "Remove"
msgstr "Usuń"
@@ -1652,39 +1678,6 @@ msgstr "Zapisz"
msgid "Preview"
msgstr "Podgląd"
-#: contrib/comments/templates/comments/moderation_queue.html:4
-#: contrib/comments/templates/comments/moderation_queue.html:19
-msgid "Comment moderation queue"
-msgstr "Kolejka moderacji komentarzy"
-
-#: contrib/comments/templates/comments/moderation_queue.html:26
-msgid "No comments to moderate"
-msgstr "Żaden komentarz nie oczekuje na akceptację"
-
-#: contrib/comments/templates/comments/moderation_queue.html:36
-msgid "Email"
-msgstr "E-mail"
-
-#: contrib/comments/templates/comments/moderation_queue.html:38
-msgid "Authenticated?"
-msgstr "Zalogowany?"
-
-#: contrib/comments/templates/comments/moderation_queue.html:39
-msgid "IP Address"
-msgstr "Adres IP"
-
-#: contrib/comments/templates/comments/moderation_queue.html:40
-msgid "Date posted"
-msgstr "Data dodania"
-
-#: contrib/comments/templates/comments/moderation_queue.html:63
-msgid "yes"
-msgstr "tak"
-
-#: contrib/comments/templates/comments/moderation_queue.html:63
-msgid "no"
-msgstr "nie"
-
#: contrib/comments/templates/comments/posted.html:4
msgid "Thanks for commenting"
msgstr "Dziękujemy za dodanie komentarza"
@@ -2599,6 +2592,10 @@ msgstr "Niepoprawna suma kontrolna numeru konta bankowego."
msgid "Enter a valid Finnish social security number."
msgstr "Wpis poprawny numer fińskiego ubezpieczenia socjalnego."
+#: contrib/localflavor/fr/forms.py:30
+msgid "Phone numbers must be in 0X XX XX XX XX format."
+msgstr "Numery telefoniczne muszą być w formacie 0X XX XX XX XX."
+
#: contrib/localflavor/in_/forms.py:14
msgid "Enter a zip code in the format XXXXXXX."
msgstr "Wpisz kod pocztowy w formacie XXXXXXX."
@@ -3944,86 +3941,86 @@ msgstr[2] ""
"Proszę podać poprawne identyfikatory %(self)s. Wartości %(value)r są "
"niepoprawne."
-#: forms/fields.py:54
+#: forms/fields.py:53
msgid "This field is required."
msgstr "To pole jest wymagane."
-#: forms/fields.py:55
+#: forms/fields.py:54
msgid "Enter a valid value."
msgstr "Wpisz poprawną wartość."
-#: forms/fields.py:138
+#: forms/fields.py:137
#, python-format
msgid "Ensure this value has at most %(max)d characters (it has %(length)d)."
msgstr ""
"Upewnij się, że ta wartość ma co najwyżej %(max)d znaków (ma długość %"
"(length)d)."
-#: forms/fields.py:139
+#: forms/fields.py:138
#, python-format
msgid "Ensure this value has at least %(min)d characters (it has %(length)d)."
msgstr ""
"Upewnij się, że ta wartość ma co najmniej %(min)d znaków (ma długość %"
"(length)d)."
-#: forms/fields.py:166
+#: forms/fields.py:165
msgid "Enter a whole number."
msgstr "Wpisz liczbę całkowitą."
-#: forms/fields.py:167 forms/fields.py:196 forms/fields.py:225
+#: forms/fields.py:166 forms/fields.py:195 forms/fields.py:224
#, python-format
msgid "Ensure this value is less than or equal to %s."
msgstr "Upewnij się, że ta wartość jest mniejsza lub równa %s."
-#: forms/fields.py:168 forms/fields.py:197 forms/fields.py:226
+#: forms/fields.py:167 forms/fields.py:196 forms/fields.py:225
#, python-format
msgid "Ensure this value is greater than or equal to %s."
msgstr "Upewnij się, że ta wartość jest większa lub równa %s."
-#: forms/fields.py:195 forms/fields.py:224
+#: forms/fields.py:194 forms/fields.py:223
msgid "Enter a number."
msgstr "Wpisz liczbę."
-#: forms/fields.py:227
+#: forms/fields.py:226
#, python-format
msgid "Ensure that there are no more than %s digits in total."
msgstr "Upewnij się, że jest nie więcej niż %s cyfr."
-#: forms/fields.py:228
+#: forms/fields.py:227
#, python-format
msgid "Ensure that there are no more than %s decimal places."
msgstr "Upewnij się, że jest nie więcej niż %s miejsc po przecinku."
-#: forms/fields.py:229
+#: forms/fields.py:228
#, python-format
msgid "Ensure that there are no more than %s digits before the decimal point."
msgstr "Upewnij się, że jest nie więcej niż %s miejsc przed przecinkiem."
-#: forms/fields.py:288 forms/fields.py:863
+#: forms/fields.py:287 forms/fields.py:862
msgid "Enter a valid date."
msgstr "Wpisz poprawną datę."
-#: forms/fields.py:322 forms/fields.py:864
+#: forms/fields.py:321 forms/fields.py:863
msgid "Enter a valid time."
msgstr "Wpisz poprawną godzinę."
-#: forms/fields.py:361
+#: forms/fields.py:360
msgid "Enter a valid date/time."
msgstr "Wpisz poprawną datę/godzinę."
-#: forms/fields.py:447
+#: forms/fields.py:446
msgid "No file was submitted. Check the encoding type on the form."
msgstr "Nie wysłano żadnego pliku. Sprawdź typ kodowania formularza."
-#: forms/fields.py:448
+#: forms/fields.py:447
msgid "No file was submitted."
msgstr "Żaden plik nie został przesłany."
-#: forms/fields.py:449
+#: forms/fields.py:448
msgid "The submitted file is empty."
msgstr "Wysłany plik jest pusty."
-#: forms/fields.py:450
+#: forms/fields.py:449
#, python-format
msgid ""
"Ensure this filename has at most %(max)d characters (it has %(length)d)."
@@ -4031,7 +4028,7 @@ msgstr ""
"Upewnij się, że nazwa tego pliku ma co najwyżej %(max)d znaków (ma długość %"
"(length)d)."
-#: forms/fields.py:483
+#: forms/fields.py:482
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -4039,29 +4036,29 @@ msgstr ""
"Wgraj poprawny plik graficzny. Ten, który został wgrany, nie jest obrazem, "
"albo jest uszkodzony."
-#: forms/fields.py:544
+#: forms/fields.py:543
msgid "Enter a valid URL."
msgstr "Wpisz poprawny URL."
-#: forms/fields.py:545
+#: forms/fields.py:544
msgid "This URL appears to be a broken link."
msgstr "Ten odnośnik jest nieprawidłowy."
-#: forms/fields.py:625 forms/fields.py:703
+#: forms/fields.py:624 forms/fields.py:702
#, python-format
msgid "Select a valid choice. %(value)s is not one of the available choices."
msgstr ""
"Wybierz poprawną wartość. %(value)s nie jest jednym z dostępnych wyborów."
-#: forms/fields.py:704 forms/fields.py:765 forms/models.py:1003
+#: forms/fields.py:703 forms/fields.py:764 forms/models.py:999
msgid "Enter a list of values."
msgstr "Podaj listę wartości."
-#: forms/fields.py:892
+#: forms/fields.py:891
msgid "Enter a valid IPv4 address."
msgstr "Wprowadź poprawny adres IPv4."
-#: forms/fields.py:902
+#: forms/fields.py:901
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr "To pole może zawierać jedynie litery, cyfry, podkreślenia i myślniki."
@@ -4070,29 +4067,29 @@ msgstr "To pole może zawierać jedynie litery, cyfry, podkreślenia i myślniki
msgid "Order"
msgstr "Porządek"
-#: forms/models.py:367
+#: forms/models.py:363
#, python-format
msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s."
msgstr ""
"Wartości w %(field_name)s muszą być unikalne dla wyszukiwań %(lookup)s w %"
"(date_field)s"
-#: forms/models.py:381 forms/models.py:389
+#: forms/models.py:377 forms/models.py:385
#, python-format
msgid "%(model_name)s with this %(field_label)s already exists."
msgstr "%(field_label)s już istnieje w %(model_name)s."
-#: forms/models.py:594
+#: forms/models.py:590
#, python-format
msgid "Please correct the duplicate data for %(field)s."
msgstr "Popraw zduplikowane dane w %(field)s."
-#: forms/models.py:598
+#: forms/models.py:594
#, python-format
msgid "Please correct the duplicate data for %(field)s, which must be unique."
msgstr "Popraw zduplikowane dane w %(field)s, które wymaga unikalności."
-#: forms/models.py:604
+#: forms/models.py:600
#, python-format
msgid ""
"Please correct the duplicate data for %(field_name)s which must be unique "
@@ -4101,24 +4098,24 @@ msgstr ""
"Popraw zduplikowane dane w %(field_name)s, które wymaga unikalności dla %"
"(lookup)s w polu %(date_field)s."
-#: forms/models.py:612
+#: forms/models.py:608
msgid "Please correct the duplicate values below."
msgstr "Popraw poniższe zduplikowane wartości."
-#: forms/models.py:867
+#: forms/models.py:863
msgid "The inline foreign key did not match the parent instance primary key."
msgstr "Osadzony klucz obcy nie pasuje do klucza głównego obiektu rodzica."
-#: forms/models.py:930
+#: forms/models.py:926
msgid "Select a valid choice. That choice is not one of the available choices."
msgstr "Wybierz poprawną wartość. Podana nie jest jednym z dostępnych wyborów."
-#: forms/models.py:1004
+#: forms/models.py:1000
#, python-format
msgid "Select a valid choice. %s is not one of the available choices."
msgstr "Wybierz poprawną wartość. %s nie jest jednym z dostępnych wyborów."
-#: forms/models.py:1006
+#: forms/models.py:1002
#, python-format
msgid "\"%s\" is not a valid value for a primary key."
msgstr "\"%s\" nie jest poprawną wartością klucza głównego."
@@ -4444,3 +4441,27 @@ msgstr "%(verbose_name)s zostało pomyślnie zmienione."
#, python-format
msgid "The %(verbose_name)s was deleted."
msgstr "%(verbose_name)s zostało usunięte."
+
+#~ msgid "Comment moderation queue"
+#~ msgstr "Kolejka moderacji komentarzy"
+
+#~ msgid "No comments to moderate"
+#~ msgstr "Żaden komentarz nie oczekuje na akceptację"
+
+#~ msgid "Email"
+#~ msgstr "E-mail"
+
+#~ msgid "Authenticated?"
+#~ msgstr "Zalogowany?"
+
+#~ msgid "IP Address"
+#~ msgstr "Adres IP"
+
+#~ msgid "Date posted"
+#~ msgstr "Data dodania"
+
+#~ msgid "yes"
+#~ msgstr "tak"
+
+#~ msgid "no"
+#~ msgstr "nie"
diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py
index bbf005ddea..9b0b516c80 100644
--- a/django/conf/project_template/settings.py
+++ b/django/conf/project_template/settings.py
@@ -60,6 +60,7 @@ TEMPLATE_LOADERS = (
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)
diff --git a/django/contrib/admin/media/css/changelists.css b/django/contrib/admin/media/css/changelists.css
index 5eb66b4d36..43033780ac 100644
--- a/django/contrib/admin/media/css/changelists.css
+++ b/django/contrib/admin/media/css/changelists.css
@@ -53,7 +53,7 @@
vertical-align: middle;
}
-#changelist table thead th:first-child {
+#changelist table thead th.action-checkbox-column {
width: 1.5em;
text-align: center;
}
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 45350e8cbb..ff716579c0 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets
from django.contrib.admin import helpers
from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict
+from django.views.decorators.csrf import csrf_protect
from django.core.exceptions import PermissionDenied
from django.db import models, transaction
from django.db.models.fields import BLANK_CHOICE_DASH
@@ -152,8 +153,9 @@ class BaseModelAdmin(object):
"""
Get a form Field for a ManyToManyField.
"""
- # If it uses an intermediary model, don't show field in admin.
- if db_field.rel.through is not None:
+ # If it uses an intermediary model that isn't auto created, don't show
+ # a field in admin.
+ if not db_field.rel.through._meta.auto_created:
return None
if db_field.name in self.raw_id_fields:
@@ -701,6 +703,8 @@ class ModelAdmin(BaseModelAdmin):
else:
return HttpResponseRedirect(".")
+ @csrf_protect
+ @transaction.commit_on_success
def add_view(self, request, form_url='', extra_context=None):
"The 'add' admin view for this model."
model = self.model
@@ -786,8 +790,9 @@ class ModelAdmin(BaseModelAdmin):
}
context.update(extra_context or {})
return self.render_change_form(request, context, form_url=form_url, add=True)
- add_view = transaction.commit_on_success(add_view)
+ @csrf_protect
+ @transaction.commit_on_success
def change_view(self, request, object_id, extra_context=None):
"The 'change' admin view for this model."
model = self.model
@@ -875,8 +880,8 @@ class ModelAdmin(BaseModelAdmin):
}
context.update(extra_context or {})
return self.render_change_form(request, context, change=True, obj=obj)
- change_view = transaction.commit_on_success(change_view)
+ @csrf_protect
def changelist_view(self, request, extra_context=None):
"The 'change list' admin view for this model."
from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
@@ -989,6 +994,7 @@ class ModelAdmin(BaseModelAdmin):
'admin/change_list.html'
], context, context_instance=context_instance)
+ @csrf_protect
def delete_view(self, request, object_id, extra_context=None):
"The 'delete' admin view for this model."
opts = self.model._meta
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
index 5f397ecb01..52ef57370d 100644
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -3,6 +3,7 @@ from django import http, template
from django.contrib.admin import ModelAdmin
from django.contrib.admin import actions
from django.contrib.auth import authenticate, login
+from django.views.decorators.csrf import csrf_protect
from django.db.models.base import ModelBase
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
@@ -186,11 +187,17 @@ class AdminSite(object):
return view(request, *args, **kwargs)
if not cacheable:
inner = never_cache(inner)
+ # We add csrf_protect here so this function can be used as a utility
+ # function for any view, without having to repeat 'csrf_protect'.
+ inner = csrf_protect(inner)
return update_wrapper(inner, view)
def get_urls(self):
from django.conf.urls.defaults import patterns, url, include
+ if settings.DEBUG:
+ self.check_dependencies()
+
def wrap(view, cacheable=False):
def wrapper(*args, **kwargs):
return self.admin_view(view, cacheable)(*args, **kwargs)
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 d28dd0f45c..11414d1465 100644
--- a/django/contrib/admin/templates/admin/auth/user/change_password.html
+++ b/django/contrib/admin/templates/admin/auth/user/change_password.html
@@ -15,7 +15,7 @@
</div>
{% endif %}{% endblock %}
{% block content %}<div id="content-main">
-<form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% block form_top %}{% endblock %}
+<form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% csrf_token %}{% block form_top %}{% endblock %}
<div>
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
{% if form.errors %}
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index f645d65a0f..c5ac729c7e 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -29,7 +29,7 @@
</ul>
{% endif %}{% endif %}
{% endblock %}
-<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% block form_top %}{% endblock %}
+<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% csrf_token %}{% block form_top %}{% endblock %}
<div>
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
{% if save_on_top %}{% submit_row %}{% endif %}
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 31bf7bd29a..20b2eff060 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -68,7 +68,7 @@
{% endif %}
{% endblock %}
- <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>
+ <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>{% csrf_token %}
{% if cl.formset %}
{{ cl.formset.management_form }}
{% endif %}
diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html
index 42802f57bc..65e73c922d 100644
--- a/django/contrib/admin/templates/admin/delete_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_confirmation.html
@@ -22,7 +22,7 @@
{% else %}
<p>{% blocktrans with object as escaped_object %}Are you sure you want to delete the {{ object_name }} "{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktrans %}</p>
<ul>{{ deleted_objects|unordered_list }}</ul>
- <form action="" method="post">
+ <form action="" method="post">{% csrf_token %}
<div>
<input type="hidden" name="post" value="yes" />
<input type="submit" value="{% trans "Yes, I'm sure" %}" />
diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
index 5550b73e2e..7f4fbc5726 100644
--- a/django/contrib/admin/templates/admin/delete_selected_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
@@ -23,7 +23,7 @@
{% for deleteable_object in deletable_objects %}
<ul>{{ deleteable_object|unordered_list }}</ul>
{% endfor %}
- <form action="" method="post">
+ <form action="" method="post">{% csrf_token %}
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}" />
diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html
index d162e5a9fa..876c4b0327 100644
--- a/django/contrib/admin/templates/admin/login.html
+++ b/django/contrib/admin/templates/admin/login.html
@@ -14,7 +14,7 @@
<p class="errornote">{{ error_message }}</p>
{% endif %}
<div id="content-main">
-<form action="{{ app_path }}" method="post" id="login-form">
+<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
<div class="form-row">
<label for="id_username">{% trans 'Username:' %}</label> <input type="text" name="username" id="id_username" />
</div>
diff --git a/django/contrib/admin/templates/admin/template_validator.html b/django/contrib/admin/templates/admin/template_validator.html
index d221807486..9a139c5d49 100644
--- a/django/contrib/admin/templates/admin/template_validator.html
+++ b/django/contrib/admin/templates/admin/template_validator.html
@@ -4,7 +4,7 @@
<div id="content-main">
-<form action="" method="post">
+<form action="" method="post">{% csrf_token %}
{% if form.errors %}
<p class="errornote">Your template had {{ form.errors|length }} error{{ form.errors|pluralize }}:</p>
diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html
index c13c7f7040..6d7a6609de 100644
--- a/django/contrib/admin/templates/registration/password_change_form.html
+++ b/django/contrib/admin/templates/registration/password_change_form.html
@@ -11,7 +11,7 @@
<p>{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}</p>
-<form action="" method="post">
+<form action="" method="post">{% csrf_token %}
{{ form.old_password.errors }}
<p class="aligned wide"><label for="id_old_password">{% trans 'Old password:' %}</label>{{ form.old_password }}</p>
diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html
index 049ee625a9..df9cf1b316 100644
--- a/django/contrib/admin/templates/registration/password_reset_confirm.html
+++ b/django/contrib/admin/templates/registration/password_reset_confirm.html
@@ -13,7 +13,7 @@
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
-<form action="" method="post">
+<form action="" method="post">{% csrf_token %}
{{ form.new_password1.errors }}
<p class="aligned wide"><label for="id_new_password1">{% trans 'New password:' %}</label>{{ form.new_password1 }}</p>
{{ form.new_password2.errors }}
diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html
index 704066c68a..d3a128428a 100644
--- a/django/contrib/admin/templates/registration/password_reset_form.html
+++ b/django/contrib/admin/templates/registration/password_reset_form.html
@@ -11,7 +11,7 @@
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}</p>
-<form action="" method="post">
+<form action="" method="post">{% csrf_token %}
{{ form.email.errors }}
<p><label for="id_email">{% trans 'E-mail address:' %}</label> {{ form.email }} <input type="submit" value="{% trans 'Reset my password' %}" /></p>
</form>
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 9a4ce3b266..5a02ab01be 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -106,6 +106,11 @@ def result_headers(cl):
else:
header = field_name
header = header.replace('_', ' ')
+ # if the field is the action checkbox: no sorting and special class
+ if field_name == 'action_checkbox':
+ yield {"text": header,
+ "class_attrib": mark_safe(' class="action-checkbox-column"')}
+ continue
# It is a non-field, but perhaps one that is sortable
admin_order_field = getattr(attr, "admin_order_field", None)
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 50e41437b8..05e5c6d300 100644
--- a/django/contrib/admin/validation.py
+++ b/django/contrib/admin/validation.py
@@ -149,12 +149,16 @@ def validate(cls, model):
validate_inline(inline, cls, model)
def validate_inline(cls, parent, parent_model):
+
# model is already verified to exist and be a Model
if cls.fk_name: # default value is None
f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name)
if not isinstance(f, models.ForeignKey):
raise ImproperlyConfigured("'%s.fk_name is not an instance of "
"models.ForeignKey." % cls.__name__)
+
+ fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True)
+
# extra = 3
# max_num = 0
for attr in ('extra', 'max_num'):
@@ -169,7 +173,6 @@ def validate_inline(cls, parent, parent_model):
# exclude
if hasattr(cls, 'exclude') and cls.exclude:
- fk = _get_foreign_key(parent_model, cls.model, can_fail=True)
if fk and fk.name in cls.exclude:
raise ImproperlyConfigured("%s cannot exclude the field "
"'%s' - this is the foreign key to the parent model "
diff --git a/django/contrib/auth/tests/remote_user.py b/django/contrib/auth/tests/remote_user.py
index 842d589a54..6115edcfd0 100644
--- a/django/contrib/auth/tests/remote_user.py
+++ b/django/contrib/auth/tests/remote_user.py
@@ -2,7 +2,7 @@ from datetime import datetime
from django.conf import settings
from django.contrib.auth.backends import RemoteUserBackend
-from django.contrib.auth.models import AnonymousUser, User
+from django.contrib.auth.models import User
from django.test import TestCase
@@ -30,15 +30,15 @@ class RemoteUserTest(TestCase):
num_users = User.objects.count()
response = self.client.get('/remote_user/')
- self.assert_(isinstance(response.context['user'], AnonymousUser))
+ self.assert_(response.context['user'].is_anonymous())
self.assertEqual(User.objects.count(), num_users)
response = self.client.get('/remote_user/', REMOTE_USER=None)
- self.assert_(isinstance(response.context['user'], AnonymousUser))
+ self.assert_(response.context['user'].is_anonymous())
self.assertEqual(User.objects.count(), num_users)
response = self.client.get('/remote_user/', REMOTE_USER='')
- self.assert_(isinstance(response.context['user'], AnonymousUser))
+ self.assert_(response.context['user'].is_anonymous())
self.assertEqual(User.objects.count(), num_users)
def test_unknown_user(self):
@@ -115,7 +115,7 @@ class RemoteUserNoCreateTest(RemoteUserTest):
def test_unknown_user(self):
num_users = User.objects.count()
response = self.client.get('/remote_user/', REMOTE_USER='newuser')
- self.assert_(isinstance(response.context['user'], AnonymousUser))
+ self.assert_(response.context['user'].is_anonymous())
self.assertEqual(User.objects.count(), num_users)
diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py
index f753ed6de8..d427874df0 100644
--- a/django/contrib/auth/views.py
+++ b/django/contrib/auth/views.py
@@ -4,6 +4,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm
from django.contrib.auth.tokens import default_token_generator
+from django.views.decorators.csrf import csrf_protect
from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response, get_object_or_404
from django.contrib.sites.models import Site, RequestSite
@@ -14,11 +15,15 @@ from django.utils.translation import ugettext as _
from django.contrib.auth.models import User
from django.views.decorators.cache import never_cache
-def login(request, template_name='registration/login.html', redirect_field_name=REDIRECT_FIELD_NAME):
+@csrf_protect
+@never_cache
+def login(request, template_name='registration/login.html',
+ redirect_field_name=REDIRECT_FIELD_NAME,
+ authentication_form=AuthenticationForm):
"Displays the login form and handles the login action."
redirect_to = request.REQUEST.get(redirect_field_name, '')
if request.method == "POST":
- form = AuthenticationForm(data=request.POST)
+ form = authentication_form(data=request.POST)
if form.is_valid():
# Light security check -- make sure redirect_to isn't garbage.
if not redirect_to or '//' in redirect_to or ' ' in redirect_to:
@@ -29,7 +34,7 @@ def login(request, template_name='registration/login.html', redirect_field_name=
request.session.delete_test_cookie()
return HttpResponseRedirect(redirect_to)
else:
- form = AuthenticationForm(request)
+ form = authentication_form(request)
request.session.set_test_cookie()
if Site._meta.installed:
current_site = Site.objects.get_current()
@@ -41,7 +46,6 @@ def login(request, template_name='registration/login.html', redirect_field_name=
'site': current_site,
'site_name': current_site.name,
}, context_instance=RequestContext(request))
-login = never_cache(login)
def logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name=REDIRECT_FIELD_NAME):
"Logs out the user and displays 'You are logged out' message."
@@ -78,6 +82,7 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N
# prompts for a new password
# - password_reset_complete shows a success message for the above
+@csrf_protect
def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html',
email_template_name='registration/password_reset_email.html',
password_reset_form=PasswordResetForm, token_generator=default_token_generator,
@@ -107,6 +112,7 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas
def password_reset_done(request, template_name='registration/password_reset_done.html'):
return render_to_response(template_name, context_instance=RequestContext(request))
+# Doesn't need csrf_protect since no-one can guess the URL
def password_reset_confirm(request, uidb36=None, token=None, template_name='registration/password_reset_confirm.html',
token_generator=default_token_generator, set_password_form=SetPasswordForm,
post_reset_redirect=None):
@@ -137,28 +143,29 @@ def password_reset_confirm(request, uidb36=None, token=None, template_name='regi
else:
context_instance['validlink'] = False
form = None
- context_instance['form'] = form
+ context_instance['form'] = form
return render_to_response(template_name, context_instance=context_instance)
def password_reset_complete(request, template_name='registration/password_reset_complete.html'):
return render_to_response(template_name, context_instance=RequestContext(request,
{'login_url': settings.LOGIN_URL}))
+@csrf_protect
+@login_required
def password_change(request, template_name='registration/password_change_form.html',
- post_change_redirect=None):
+ post_change_redirect=None, password_change_form=PasswordChangeForm):
if post_change_redirect is None:
post_change_redirect = reverse('django.contrib.auth.views.password_change_done')
if request.method == "POST":
- form = PasswordChangeForm(request.user, request.POST)
+ form = password_change_form(user=request.user, data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(post_change_redirect)
else:
- form = PasswordChangeForm(request.user)
+ form = password_change_form(user=request.user)
return render_to_response(template_name, {
'form': form,
}, context_instance=RequestContext(request))
-password_change = login_required(password_change)
def password_change_done(request, template_name='registration/password_change_done.html'):
return render_to_response(template_name, context_instance=RequestContext(request))
diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py
index c2f8e564f4..ede833f530 100644
--- a/django/contrib/comments/admin.py
+++ b/django/contrib/comments/admin.py
@@ -1,7 +1,8 @@
from django.contrib import admin
from django.contrib.comments.models import Comment
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _, ungettext
from django.contrib.comments import get_model
+from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete
class CommentsAdmin(admin.ModelAdmin):
fieldsets = (
@@ -22,6 +23,44 @@ class CommentsAdmin(admin.ModelAdmin):
ordering = ('-submit_date',)
raw_id_fields = ('user',)
search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address')
+ actions = ["flag_comments", "approve_comments", "remove_comments"]
+
+ def get_actions(self, request):
+ actions = super(CommentsAdmin, self).get_actions(request)
+ # Only superusers should be able to delete the comments from the DB.
+ if not request.user.is_superuser:
+ actions.pop('delete_selected')
+ if not request.user.has_perm('comments.can_moderate'):
+ actions.pop('approve_comments')
+ actions.pop('remove_comments')
+ return actions
+
+ def flag_comments(self, request, queryset):
+ self._bulk_flag(request, queryset, perform_flag, _("flagged"))
+ flag_comments.short_description = _("Flag selected comments")
+
+ def approve_comments(self, request, queryset):
+ self._bulk_flag(request, queryset, perform_approve, _('approved'))
+ approve_comments.short_description = _("Approve selected comments")
+
+ def remove_comments(self, request, queryset):
+ self._bulk_flag(request, queryset, perform_delete, _('removed'))
+ remove_comments.short_description = _("Remove selected comments")
+
+ def _bulk_flag(self, request, queryset, action, description):
+ """
+ Flag, approve, or remove some comments from an admin action. Actually
+ calls the `action` argument to perform the heavy lifting.
+ """
+ n_comments = 0
+ for comment in queryset:
+ action(request, comment)
+ n_comments += 1
+
+ msg = ungettext(u'1 comment was successfully %(action)s.',
+ u'%(count)s comments were successfully %(action)s.',
+ n_comments)
+ self.message_user(request, msg % {'count': n_comments, 'action': description})
# Only register the default admin if the model is the built-in comment model
# (this won't be true if there's a custom comment app).
diff --git a/django/contrib/comments/templates/comments/approve.html b/django/contrib/comments/templates/comments/approve.html
index a4306a6fc2..1a3a3fd80c 100644
--- a/django/contrib/comments/templates/comments/approve.html
+++ b/django/contrib/comments/templates/comments/approve.html
@@ -6,7 +6,7 @@
{% block content %}
<h1>{% trans "Really make this comment public?" %}</h1>
<blockquote>{{ comment|linebreaks }}</blockquote>
- <form action="." method="post">
+ <form action="." method="post">{% csrf_token %}
{% if next %}<input type="hidden" name="next" value="{{ next }}" id="next" />{% endif %}
<p class="submit">
<input type="submit" name="submit" value="{% trans "Approve" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a>
diff --git a/django/contrib/comments/templates/comments/delete.html b/django/contrib/comments/templates/comments/delete.html
index 7d73eac979..5ff2add9c5 100644
--- a/django/contrib/comments/templates/comments/delete.html
+++ b/django/contrib/comments/templates/comments/delete.html
@@ -6,7 +6,7 @@
{% block content %}
<h1>{% trans "Really remove this comment?" %}</h1>
<blockquote>{{ comment|linebreaks }}</blockquote>
- <form action="." method="post">
+ <form action="." method="post">{% csrf_token %}
{% if next %}<input type="hidden" name="next" value="{{ next }}" id="next" />{% endif %}
<p class="submit">
<input type="submit" name="submit" value="{% trans "Remove" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a>
diff --git a/django/contrib/comments/templates/comments/flag.html b/django/contrib/comments/templates/comments/flag.html
index 08dbe0b0b0..0b9ab1ccb2 100644
--- a/django/contrib/comments/templates/comments/flag.html
+++ b/django/contrib/comments/templates/comments/flag.html
@@ -6,7 +6,7 @@
{% block content %}
<h1>{% trans "Really flag this comment?" %}</h1>
<blockquote>{{ comment|linebreaks }}</blockquote>
- <form action="." method="post">
+ <form action="." method="post">{% csrf_token %}
{% if next %}<input type="hidden" name="next" value="{{ next }}" id="next" />{% endif %}
<p class="submit">
<input type="submit" name="submit" value="{% trans "Flag" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a>
diff --git a/django/contrib/comments/templates/comments/form.html b/django/contrib/comments/templates/comments/form.html
index d8e248372f..30f031128c 100644
--- a/django/contrib/comments/templates/comments/form.html
+++ b/django/contrib/comments/templates/comments/form.html
@@ -1,5 +1,5 @@
{% load comments i18n %}
-<form action="{% comment_form_target %}" method="post">
+<form action="{% comment_form_target %}" method="post">{% csrf_token %}
{% if next %}<input type="hidden" name="next" value="{{ next }}" />{% endif %}
{% for field in form %}
{% if field.is_hidden %}
diff --git a/django/contrib/comments/templates/comments/moderation_queue.html b/django/contrib/comments/templates/comments/moderation_queue.html
deleted file mode 100644
index 73012b3539..0000000000
--- a/django/contrib/comments/templates/comments/moderation_queue.html
+++ /dev/null
@@ -1,75 +0,0 @@
-{% extends "admin/change_list.html" %}
-{% load adminmedia i18n %}
-
-{% block title %}{% trans "Comment moderation queue" %}{% endblock %}
-
-{% block extrahead %}
- {{ block.super }}
- <style type="text/css" media="screen">
- p#nocomments { font-size: 200%; text-align: center; border: 1px #ccc dashed; padding: 4em; }
- td.actions { width: 11em; }
- td.actions form { display: inline; }
- td.actions form input.submit { width: 5em; padding: 2px 4px; margin-right: 4px;}
- td.actions form input.approve { background: green; color: white; }
- td.actions form input.remove { background: red; color: white; }
- </style>
-{% endblock %}
-
-{% block branding %}
-<h1 id="site-name">{% trans "Comment moderation queue" %}</h1>
-{% endblock %}
-
-{% block breadcrumbs %}{% endblock %}
-
-{% block content %}
-{% if empty %}
-<p id="nocomments">{% trans "No comments to moderate" %}.</p>
-{% else %}
-<div id="content-main">
- <div class="module" id="changelist">
- <table cellspacing="0">
- <thead>
- <tr>
- <th>{% trans "Action" %}</th>
- <th>{% trans "Name" %}</th>
- <th>{% trans "Comment" %}</th>
- <th>{% trans "Email" %}</th>
- <th>{% trans "URL" %}</th>
- <th>{% trans "Authenticated?" %}</th>
- <th>{% trans "IP Address" %}</th>
- <th class="sorted desc">{% trans "Date posted" %}</th>
- </tr>
- </thead>
- <tbody>
- {% for comment in comments %}
- <tr class="{% cycle 'row1' 'row2' %}">
- <td class="actions">
- <form action="{% url comments-approve comment.pk %}" method="post">
- <input type="hidden" name="next" value="{% url comments-moderation-queue %}" />
- <input class="approve submit" type="submit" name="submit" value="{% trans "Approve" %}" />
- </form>
- <form action="{% url comments-delete comment.pk %}" method="post">
- <input type="hidden" name="next" value="{% url comments-moderation-queue %}" />
- <input class="remove submit" type="submit" name="submit" value="{% trans "Remove" %}" />
- </form>
- </td>
- <td>{{ comment.name }}</td>
- <td>{{ comment.comment|truncatewords:"50" }}</td>
- <td>{{ comment.email }}</td>
- <td>{{ comment.url }}</td>
- <td>
- <img
- src="{% admin_media_prefix %}img/admin/icon-{% if comment.user %}yes{% else %}no{% endif %}.gif"
- alt="{% if comment.user %}{% trans "yes" %}{% else %}{% trans "no" %}{% endif %}"
- />
- </td>
- <td>{{ comment.ip_address }}</td>
- <td>{{ comment.submit_date|date:"F j, P" }}</td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </div>
-</div>
-{% endif %}
-{% endblock %}
diff --git a/django/contrib/comments/templates/comments/preview.html b/django/contrib/comments/templates/comments/preview.html
index d3884575f5..1b072a76f0 100644
--- a/django/contrib/comments/templates/comments/preview.html
+++ b/django/contrib/comments/templates/comments/preview.html
@@ -5,7 +5,7 @@
{% block content %}
{% load comments %}
- <form action="{% comment_form_target %}" method="post">
+ <form action="{% comment_form_target %}" method="post">{% csrf_token %}
{% if next %}<input type="hidden" name="next" value="{{ next }}" />{% endif %}
{% if form.errors %}
<h1>{% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}</h1>
diff --git a/django/contrib/comments/urls.py b/django/contrib/comments/urls.py
index 5caef9c7d4..2bfefa3e2d 100644
--- a/django/contrib/comments/urls.py
+++ b/django/contrib/comments/urls.py
@@ -7,7 +7,6 @@ urlpatterns = patterns('django.contrib.comments.views',
url(r'^flagged/$', 'moderation.flag_done', name='comments-flag-done'),
url(r'^delete/(\d+)/$', 'moderation.delete', name='comments-delete'),
url(r'^deleted/$', 'moderation.delete_done', name='comments-delete-done'),
- url(r'^moderate/$', 'moderation.moderation_queue', name='comments-moderation-queue'),
url(r'^approve/(\d+)/$', 'moderation.approve', name='comments-approve'),
url(r'^approved/$', 'moderation.approve_done', name='comments-approve-done'),
)
diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py
index 89a3dd9bba..7fbe80eead 100644
--- a/django/contrib/comments/views/comments.py
+++ b/django/contrib/comments/views/comments.py
@@ -10,6 +10,7 @@ from django.utils.html import escape
from django.views.decorators.http import require_POST
from django.contrib import comments
from django.contrib.comments import signals
+from django.views.decorators.csrf import csrf_protect
class CommentPostBadRequest(http.HttpResponseBadRequest):
"""
@@ -22,6 +23,8 @@ class CommentPostBadRequest(http.HttpResponseBadRequest):
if settings.DEBUG:
self.content = render_to_string("comments/400-debug.html", {"why": why})
+@csrf_protect
+@require_POST
def post_comment(request, next=None):
"""
Post a comment.
@@ -116,8 +119,6 @@ def post_comment(request, next=None):
return next_redirect(data, next, comment_done, c=comment._get_pk_val())
-post_comment = require_POST(post_comment)
-
comment_done = confirmation_view(
template = "comments/posted.html",
doc = """Display a "comment was posted" success page."""
diff --git a/django/contrib/comments/views/moderation.py b/django/contrib/comments/views/moderation.py
index 3334b0927e..73304ba416 100644
--- a/django/contrib/comments/views/moderation.py
+++ b/django/contrib/comments/views/moderation.py
@@ -3,12 +3,12 @@ from django.conf import settings
from django.shortcuts import get_object_or_404, render_to_response
from django.contrib.auth.decorators import login_required, permission_required
from utils import next_redirect, confirmation_view
-from django.core.paginator import Paginator, InvalidPage
-from django.http import Http404
from django.contrib import comments
from django.contrib.comments import signals
+from django.views.decorators.csrf import csrf_protect
-#@login_required
+@csrf_protect
+@login_required
def flag(request, comment_id, next=None):
"""
Flags a comment. Confirmation on GET, action on POST.
@@ -22,18 +22,7 @@ def flag(request, comment_id, next=None):
# Flag on POST
if request.method == 'POST':
- flag, created = comments.models.CommentFlag.objects.get_or_create(
- comment = comment,
- user = request.user,
- flag = comments.models.CommentFlag.SUGGEST_REMOVAL
- )
- signals.comment_was_flagged.send(
- sender = comment.__class__,
- comment = comment,
- flag = flag,
- created = created,
- request = request,
- )
+ perform_flag(request, comment)
return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk)
# Render a form on GET
@@ -42,9 +31,9 @@ def flag(request, comment_id, next=None):
{'comment': comment, "next": next},
template.RequestContext(request)
)
-flag = login_required(flag)
-#@permission_required("comments.delete_comment")
+@csrf_protect
+@permission_required("comments.can_moderate")
def delete(request, comment_id, next=None):
"""
Deletes a comment. Confirmation on GET, action on POST. Requires the "can
@@ -60,20 +49,7 @@ def delete(request, comment_id, next=None):
# Delete on POST
if request.method == 'POST':
# Flag the comment as deleted instead of actually deleting it.
- flag, created = comments.models.CommentFlag.objects.get_or_create(
- comment = comment,
- user = request.user,
- flag = comments.models.CommentFlag.MODERATOR_DELETION
- )
- comment.is_removed = True
- comment.save()
- signals.comment_was_flagged.send(
- sender = comment.__class__,
- comment = comment,
- flag = flag,
- created = created,
- request = request,
- )
+ perform_delete(request, comment)
return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk)
# Render a form on GET
@@ -82,9 +58,9 @@ def delete(request, comment_id, next=None):
{'comment': comment, "next": next},
template.RequestContext(request)
)
-delete = permission_required("comments.can_moderate")(delete)
-#@permission_required("comments.can_moderate")
+@csrf_protect
+@permission_required("comments.can_moderate")
def approve(request, comment_id, next=None):
"""
Approve a comment (that is, mark it as public and non-removed). Confirmation
@@ -100,23 +76,7 @@ def approve(request, comment_id, next=None):
# Delete on POST
if request.method == 'POST':
# Flag the comment as approved.
- flag, created = comments.models.CommentFlag.objects.get_or_create(
- comment = comment,
- user = request.user,
- flag = comments.models.CommentFlag.MODERATOR_APPROVAL,
- )
-
- comment.is_removed = False
- comment.is_public = True
- comment.save()
-
- signals.comment_was_flagged.send(
- sender = comment.__class__,
- comment = comment,
- flag = flag,
- created = created,
- request = request,
- )
+ perform_approve(request, comment)
return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk)
# Render a form on GET
@@ -126,69 +86,64 @@ def approve(request, comment_id, next=None):
template.RequestContext(request)
)
-approve = permission_required("comments.can_moderate")(approve)
-
+# The following functions actually perform the various flag/aprove/delete
+# actions. They've been broken out into seperate functions to that they
+# may be called from admin actions.
-#@permission_required("comments.can_moderate")
-def moderation_queue(request):
+def perform_flag(request, comment):
"""
- Displays a list of unapproved comments to be approved.
+ Actually perform the flagging of a comment from a request.
+ """
+ flag, created = comments.models.CommentFlag.objects.get_or_create(
+ comment = comment,
+ user = request.user,
+ flag = comments.models.CommentFlag.SUGGEST_REMOVAL
+ )
+ signals.comment_was_flagged.send(
+ sender = comment.__class__,
+ comment = comment,
+ flag = flag,
+ created = created,
+ request = request,
+ )
- Templates: `comments/moderation_queue.html`
- Context:
- comments
- Comments to be approved (paginated).
- empty
- Is the comment list empty?
- is_paginated
- Is there more than one page?
- results_per_page
- Number of comments per page
- has_next
- Is there a next page?
- has_previous
- Is there a previous page?
- page
- The current page number
- next
- The next page number
- pages
- Number of pages
- hits
- Total number of comments
- page_range
- Range of page numbers
+def perform_delete(request, comment):
+ flag, created = comments.models.CommentFlag.objects.get_or_create(
+ comment = comment,
+ user = request.user,
+ flag = comments.models.CommentFlag.MODERATOR_DELETION
+ )
+ comment.is_removed = True
+ comment.save()
+ signals.comment_was_flagged.send(
+ sender = comment.__class__,
+ comment = comment,
+ flag = flag,
+ created = created,
+ request = request,
+ )
- """
- qs = comments.get_model().objects.filter(is_public=False, is_removed=False)
- paginator = Paginator(qs, 100)
- try:
- page = int(request.GET.get("page", 1))
- except ValueError:
- raise Http404
+def perform_approve(request, comment):
+ flag, created = comments.models.CommentFlag.objects.get_or_create(
+ comment = comment,
+ user = request.user,
+ flag = comments.models.CommentFlag.MODERATOR_APPROVAL,
+ )
- try:
- comments_per_page = paginator.page(page)
- except InvalidPage:
- raise Http404
+ comment.is_removed = False
+ comment.is_public = True
+ comment.save()
- return render_to_response("comments/moderation_queue.html", {
- 'comments' : comments_per_page.object_list,
- 'empty' : page == 1 and paginator.count == 0,
- 'is_paginated': paginator.num_pages > 1,
- 'results_per_page': 100,
- 'has_next': comments_per_page.has_next(),
- 'has_previous': comments_per_page.has_previous(),
- 'page': page,
- 'next': page + 1,
- 'previous': page - 1,
- 'pages': paginator.num_pages,
- 'hits' : paginator.count,
- 'page_range' : paginator.page_range
- }, context_instance=template.RequestContext(request))
+ signals.comment_was_flagged.send(
+ sender = comment.__class__,
+ comment = comment,
+ flag = flag,
+ created = created,
+ request = request,
+ )
-moderation_queue = permission_required("comments.can_moderate")(moderation_queue)
+# Confirmation views.
flag_done = confirmation_view(
template = "comments/flagged.html",
diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
index 40a96e534c..27d44cc5e5 100644
--- a/django/contrib/contenttypes/generic.py
+++ b/django/contrib/contenttypes/generic.py
@@ -105,8 +105,6 @@ class GenericRelation(RelatedField, Field):
limit_choices_to=kwargs.pop('limit_choices_to', None),
symmetrical=kwargs.pop('symmetrical', True))
- # By its very nature, a GenericRelation doesn't create a table.
- self.creates_table = False
# Override content-type/object-id field names on the related class
self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
diff --git a/django/contrib/csrf/middleware.py b/django/contrib/csrf/middleware.py
index 0d0a8eca9e..4885cfcc3e 100644
--- a/django/contrib/csrf/middleware.py
+++ b/django/contrib/csrf/middleware.py
@@ -1,160 +1,7 @@
-"""
-Cross Site Request Forgery Middleware.
+from django.middleware.csrf import CsrfMiddleware, CsrfViewMiddleware, CsrfResponseMiddleware
+from django.views.decorators.csrf import csrf_exempt, csrf_view_exempt, csrf_response_exempt
-This module provides a middleware that implements protection
-against request forgeries from other sites.
-"""
-
-import re
-import itertools
-try:
- from functools import wraps
-except ImportError:
- from django.utils.functional import wraps # Python 2.3, 2.4 fallback.
-
-from django.conf import settings
-from django.http import HttpResponseForbidden
-from django.utils.hashcompat import md5_constructor
-from django.utils.safestring import mark_safe
-
-_ERROR_MSG = mark_safe('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><body><h1>403 Forbidden</h1><p>Cross Site Request Forgery detected. Request aborted.</p></body></html>')
-
-_POST_FORM_RE = \
- re.compile(r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE)
-
-_HTML_TYPES = ('text/html', 'application/xhtml+xml')
-
-def _make_token(session_id):
- return md5_constructor(settings.SECRET_KEY + session_id).hexdigest()
-
-class CsrfViewMiddleware(object):
- """
- Middleware that requires a present and correct csrfmiddlewaretoken
- for POST requests that have an active session.
- """
- def process_view(self, request, callback, callback_args, callback_kwargs):
- if request.method == 'POST':
- if getattr(callback, 'csrf_exempt', False):
- return None
-
- if request.is_ajax():
- return None
-
- try:
- session_id = request.COOKIES[settings.SESSION_COOKIE_NAME]
- except KeyError:
- # No session, no check required
- return None
-
- csrf_token = _make_token(session_id)
- # check incoming token
- try:
- request_csrf_token = request.POST['csrfmiddlewaretoken']
- except KeyError:
- return HttpResponseForbidden(_ERROR_MSG)
-
- if request_csrf_token != csrf_token:
- return HttpResponseForbidden(_ERROR_MSG)
-
- return None
-
-class CsrfResponseMiddleware(object):
- """
- Middleware that post-processes a response to add a
- csrfmiddlewaretoken if the response/request have an active
- session.
- """
- def process_response(self, request, response):
- if getattr(response, 'csrf_exempt', False):
- return response
-
- csrf_token = None
- try:
- # This covers a corner case in which the outgoing response
- # both contains a form and sets a session cookie. This
- # really should not be needed, since it is best if views
- # that create a new session (login pages) also do a
- # redirect, as is done by all such view functions in
- # Django.
- cookie = response.cookies[settings.SESSION_COOKIE_NAME]
- csrf_token = _make_token(cookie.value)
- except KeyError:
- # Normal case - look for existing session cookie
- try:
- session_id = request.COOKIES[settings.SESSION_COOKIE_NAME]
- csrf_token = _make_token(session_id)
- except KeyError:
- # no incoming or outgoing cookie
- pass
-
- if csrf_token is not None and \
- response['Content-Type'].split(';')[0] in _HTML_TYPES:
-
- # ensure we don't add the 'id' attribute twice (HTML validity)
- idattributes = itertools.chain(("id='csrfmiddlewaretoken'",),
- itertools.repeat(''))
- def add_csrf_field(match):
- """Returns the matched <form> tag plus the added <input> element"""
- return mark_safe(match.group() + "<div style='display:none;'>" + \
- "<input type='hidden' " + idattributes.next() + \
- " name='csrfmiddlewaretoken' value='" + csrf_token + \
- "' /></div>")
-
- # Modify any POST forms
- response.content = _POST_FORM_RE.sub(add_csrf_field, response.content)
- return response
-
-class CsrfMiddleware(CsrfViewMiddleware, CsrfResponseMiddleware):
- """Django middleware that adds protection against Cross Site
- Request Forgeries by adding hidden form fields to POST forms and
- checking requests for the correct value.
-
- In the list of middlewares, SessionMiddleware is required, and
- must come after this middleware. CsrfMiddleWare must come after
- compression middleware.
-
- If a session ID cookie is present, it is hashed with the
- SECRET_KEY setting to create an authentication token. This token
- is added to all outgoing POST forms and is expected on all
- incoming POST requests that have a session ID cookie.
-
- If you are setting cookies directly, instead of using Django's
- session framework, this middleware will not work.
-
- CsrfMiddleWare is composed of two middleware, CsrfViewMiddleware
- and CsrfResponseMiddleware which can be used independently.
- """
- pass
-
-def csrf_response_exempt(view_func):
- """
- Modifies a view function so that its response is exempt
- from the post-processing of the CSRF middleware.
- """
- def wrapped_view(*args, **kwargs):
- resp = view_func(*args, **kwargs)
- resp.csrf_exempt = True
- return resp
- return wraps(view_func)(wrapped_view)
-
-def csrf_view_exempt(view_func):
- """
- Marks a view function as being exempt from CSRF view protection.
- """
- # We could just do view_func.csrf_exempt = True, but decorators
- # are nicer if they don't have side-effects, so we return a new
- # function.
- def wrapped_view(*args, **kwargs):
- return view_func(*args, **kwargs)
- wrapped_view.csrf_exempt = True
- return wraps(view_func)(wrapped_view)
-
-def csrf_exempt(view_func):
- """
- Marks a view function as being exempt from the CSRF checks
- and post processing.
-
- This is the same as using both the csrf_view_exempt and
- csrf_response_exempt decorators.
- """
- return csrf_response_exempt(csrf_view_exempt(view_func))
+import warnings
+warnings.warn("This import for CSRF functionality is deprecated. Please use django.middleware.csrf for the middleware and django.views.decorators.csrf for decorators.",
+ PendingDeprecationWarning
+ )
diff --git a/django/contrib/csrf/tests.py b/django/contrib/csrf/tests.py
deleted file mode 100644
index 3c533a01e6..0000000000
--- a/django/contrib/csrf/tests.py
+++ /dev/null
@@ -1,144 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from django.test import TestCase
-from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
-from django.contrib.csrf.middleware import CsrfMiddleware, _make_token, csrf_exempt
-from django.conf import settings
-
-
-def post_form_response():
- resp = HttpResponse(content="""
-<html><body><form method="POST"><input type="text" /></form></body></html>
-""", mimetype="text/html")
- return resp
-
-def test_view(request):
- return post_form_response()
-
-class CsrfMiddlewareTest(TestCase):
-
- _session_id = "1"
-
- def _get_GET_no_session_request(self):
- return HttpRequest()
-
- def _get_GET_session_request(self):
- req = self._get_GET_no_session_request()
- req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id
- return req
-
- def _get_POST_session_request(self):
- req = self._get_GET_session_request()
- req.method = "POST"
- return req
-
- def _get_POST_no_session_request(self):
- req = self._get_GET_no_session_request()
- req.method = "POST"
- return req
-
- def _get_POST_session_request_with_token(self):
- req = self._get_POST_session_request()
- req.POST['csrfmiddlewaretoken'] = _make_token(self._session_id)
- return req
-
- def _get_post_form_response(self):
- return post_form_response()
-
- def _get_new_session_response(self):
- resp = self._get_post_form_response()
- resp.cookies[settings.SESSION_COOKIE_NAME] = self._session_id
- return resp
-
- def _check_token_present(self, response):
- self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(self._session_id))
-
- def get_view(self):
- return test_view
-
- # Check the post processing
- def test_process_response_no_session(self):
- """
- Check the post-processor does nothing if no session active
- """
- req = self._get_GET_no_session_request()
- resp = self._get_post_form_response()
- resp_content = resp.content # needed because process_response modifies resp
- resp2 = CsrfMiddleware().process_response(req, resp)
- self.assertEquals(resp_content, resp2.content)
-
- def test_process_response_existing_session(self):
- """
- Check that the token is inserted if there is an existing session
- """
- req = self._get_GET_session_request()
- resp = self._get_post_form_response()
- resp_content = resp.content # needed because process_response modifies resp
- resp2 = CsrfMiddleware().process_response(req, resp)
- self.assertNotEqual(resp_content, resp2.content)
- self._check_token_present(resp2)
-
- def test_process_response_new_session(self):
- """
- Check that the token is inserted if there is a new session being started
- """
- req = self._get_GET_no_session_request() # no session in request
- resp = self._get_new_session_response() # but new session started
- resp_content = resp.content # needed because process_response modifies resp
- resp2 = CsrfMiddleware().process_response(req, resp)
- self.assertNotEqual(resp_content, resp2.content)
- self._check_token_present(resp2)
-
- def test_process_response_exempt_view(self):
- """
- Check that no post processing is done for an exempt view
- """
- req = self._get_POST_session_request()
- resp = csrf_exempt(self.get_view())(req)
- resp_content = resp.content
- resp2 = CsrfMiddleware().process_response(req, resp)
- self.assertEquals(resp_content, resp2.content)
-
- # Check the request processing
- def test_process_request_no_session(self):
- """
- Check that if no session is present, the middleware does nothing.
- to the incoming request.
- """
- req = self._get_POST_no_session_request()
- req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
- self.assertEquals(None, req2)
-
- def test_process_request_session_no_token(self):
- """
- Check that if a session is present but no token, we get a 'forbidden'
- """
- req = self._get_POST_session_request()
- req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
- self.assertEquals(HttpResponseForbidden, req2.__class__)
-
- def test_process_request_session_and_token(self):
- """
- Check that if a session is present and a token, the middleware lets it through
- """
- req = self._get_POST_session_request_with_token()
- req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
- self.assertEquals(None, req2)
-
- def test_process_request_session_no_token_exempt_view(self):
- """
- Check that if a session is present and no token, but the csrf_exempt
- decorator has been applied to the view, the middleware lets it through
- """
- req = self._get_POST_session_request()
- req2 = CsrfMiddleware().process_view(req, csrf_exempt(self.get_view()), (), {})
- self.assertEquals(None, req2)
-
- def test_ajax_exemption(self):
- """
- Check that AJAX requests are automatically exempted.
- """
- req = self._get_POST_session_request()
- req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
- self.assertEquals(None, req2)
diff --git a/django/contrib/formtools/templates/formtools/form.html b/django/contrib/formtools/templates/formtools/form.html
index 194bbdd675..2f2de1f637 100644
--- a/django/contrib/formtools/templates/formtools/form.html
+++ b/django/contrib/formtools/templates/formtools/form.html
@@ -4,7 +4,7 @@
{% if form.errors %}<h1>Please correct the following errors</h1>{% else %}<h1>Submit</h1>{% endif %}
-<form action="" method="post">
+<form action="" method="post">{% csrf_token %}
<table>
{{ form }}
</table>
diff --git a/django/contrib/formtools/templates/formtools/preview.html b/django/contrib/formtools/templates/formtools/preview.html
index c53ce91724..eb88b1ec2e 100644
--- a/django/contrib/formtools/templates/formtools/preview.html
+++ b/django/contrib/formtools/templates/formtools/preview.html
@@ -15,7 +15,7 @@
<p>Security hash: {{ hash_value }}</p>
-<form action="" method="post">
+<form action="" method="post">{% csrf_token %}
{% for field in form %}{{ field.as_hidden }}
{% endfor %}
<input type="hidden" name="{{ stage_field }}" value="2" />
@@ -25,7 +25,7 @@
<h1>Or edit it again</h1>
-<form action="" method="post">
+<form action="" method="post">{% csrf_token %}
<table>
{{ form }}
</table>
diff --git a/django/contrib/formtools/tests.py b/django/contrib/formtools/tests.py
index 86d40b963b..bc65a60fbe 100644
--- a/django/contrib/formtools/tests.py
+++ b/django/contrib/formtools/tests.py
@@ -147,15 +147,18 @@ class WizardPageTwoForm(forms.Form):
class WizardClass(wizard.FormWizard):
def render_template(self, *args, **kw):
- return ""
+ return http.HttpResponse("")
def done(self, request, cleaned_data):
return http.HttpResponse(success_string)
-class DummyRequest(object):
+class DummyRequest(http.HttpRequest):
def __init__(self, POST=None):
+ super(DummyRequest, self).__init__()
self.method = POST and "POST" or "GET"
- self.POST = POST
+ if POST is not None:
+ self.POST.update(POST)
+ self._dont_enforce_csrf_checks = True
class WizardTests(TestCase):
def test_step_starts_at_zero(self):
diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py
index b075628c49..4729e155b8 100644
--- a/django/contrib/formtools/wizard.py
+++ b/django/contrib/formtools/wizard.py
@@ -14,6 +14,7 @@ from django.template.context import RequestContext
from django.utils.hashcompat import md5_constructor
from django.utils.translation import ugettext_lazy as _
from django.contrib.formtools.utils import security_hash
+from django.views.decorators.csrf import csrf_protect
class FormWizard(object):
# Dictionary of extra template context variables.
@@ -44,6 +45,7 @@ class FormWizard(object):
# hook methods might alter self.form_list.
return len(self.form_list)
+ @csrf_protect
def __call__(self, request, *args, **kwargs):
"""
Main method that does all the hard work, conforming to the Django view
diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py
index 05b824b95d..91d6a6493d 100644
--- a/django/contrib/gis/gdal/geometries.py
+++ b/django/contrib/gis/gdal/geometries.py
@@ -29,7 +29,7 @@
+proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs
>>> print mpnt
MULTIPOINT (-89.999930378602485 29.999797886557641,-89.999930378602485 29.999797886557641)
-
+
The OGRGeomType class is to make it easy to specify an OGR geometry type:
>>> from django.contrib.gis.gdal import OGRGeomType
>>> gt1 = OGRGeomType(3) # Using an integer for the type
@@ -78,7 +78,7 @@ class OGRGeometry(GDALBase):
geom_input = buffer(a2b_hex(geom_input.upper()))
str_instance = False
- # Constructing the geometry,
+ # Constructing the geometry,
if str_instance:
# Checking if unicode
if isinstance(geom_input, unicode):
@@ -130,12 +130,12 @@ class OGRGeometry(GDALBase):
self.__class__ = GEO_CLASSES[self.geom_type.num]
@classmethod
- def from_bbox(cls, bbox):
+ def from_bbox(cls, bbox):
"Constructs a Polygon from a bounding box (4-tuple)."
x0, y0, x1, y1 = bbox
return OGRGeometry( 'POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s))' % (
x0, y0, x0, y1, x1, y1, x1, y0, x0, y0) )
-
+
def __del__(self):
"Deletes this Geometry."
if self._ptr: capi.destroy_geom(self._ptr)
@@ -179,10 +179,17 @@ class OGRGeometry(GDALBase):
"Returns 0 for points, 1 for lines, and 2 for surfaces."
return capi.get_dims(self.ptr)
- @property
- def coord_dim(self):
+ def _get_coord_dim(self):
"Returns the coordinate dimension of the Geometry."
- return capi.get_coord_dims(self.ptr)
+ return capi.get_coord_dim(self.ptr)
+
+ def _set_coord_dim(self, dim):
+ "Sets the coordinate dimension of this Geometry."
+ if not dim in (2, 3):
+ raise ValueError('Geometry dimension must be either 2 or 3')
+ capi.set_coord_dim(self.ptr, dim)
+
+ coord_dim = property(_get_coord_dim, _set_coord_dim)
@property
def geom_count(self):
@@ -237,7 +244,7 @@ class OGRGeometry(GDALBase):
return self.envelope.tuple
#### SpatialReference-related Properties ####
-
+
# The SRS property
def _get_srs(self):
"Returns the Spatial Reference for this Geometry."
@@ -249,11 +256,15 @@ class OGRGeometry(GDALBase):
def _set_srs(self, srs):
"Sets the SpatialReference for this geometry."
+ # Do not have to clone the `SpatialReference` object pointer because
+ # when it is assigned to this `OGRGeometry` it's internal OGR
+ # reference count is incremented, and will likewise be released
+ # (decremented) when this geometry's destructor is called.
if isinstance(srs, SpatialReference):
- srs_ptr = srs_api.clone_srs(srs.ptr)
+ srs_ptr = srs.ptr
elif isinstance(srs, (int, long, basestring)):
sr = SpatialReference(srs)
- srs_ptr = srs_api.clone_srs(sr.ptr)
+ srs_ptr = sr.ptr
else:
raise TypeError('Cannot assign spatial reference with object of type: %s' % type(srs))
capi.assign_srs(self.ptr, srs_ptr)
@@ -298,7 +309,7 @@ class OGRGeometry(GDALBase):
Returns the GeoJSON representation of this Geometry (requires
GDAL 1.5+).
"""
- if GEOJSON:
+ if GEOJSON:
return capi.to_json(self.ptr)
else:
raise NotImplementedError('GeoJSON output only supported on GDAL 1.5+.')
@@ -335,7 +346,7 @@ class OGRGeometry(GDALBase):
def wkt(self):
"Returns the WKT representation of the Geometry."
return capi.to_wkt(self.ptr, byref(c_char_p()))
-
+
#### Geometry Methods ####
def clone(self):
"Clones this OGR Geometry."
@@ -363,6 +374,16 @@ class OGRGeometry(GDALBase):
klone = self.clone()
klone.transform(coord_trans)
return klone
+
+ # Have to get the coordinate dimension of the original geometry
+ # so it can be used to reset the transformed geometry's dimension
+ # afterwards. This is done because of GDAL bug (in versions prior
+ # to 1.7) that turns geometries 3D after transformation, see:
+ # http://trac.osgeo.org/gdal/changeset/17792
+ orig_dim = self.coord_dim
+
+ # Depending on the input type, use the appropriate OGR routine
+ # to perform the transformation.
if isinstance(coord_trans, CoordTransform):
capi.geom_transform(self.ptr, coord_trans.ptr)
elif isinstance(coord_trans, SpatialReference):
@@ -373,6 +394,10 @@ class OGRGeometry(GDALBase):
else:
raise TypeError('Transform only accepts CoordTransform, SpatialReference, string, and integer objects.')
+ # Setting with original dimension, see comment above.
+ if self.coord_dim != orig_dim:
+ self.coord_dim = orig_dim
+
def transform_to(self, srs):
"For backwards-compatibility."
self.transform(srs)
@@ -391,7 +416,7 @@ class OGRGeometry(GDALBase):
def intersects(self, other):
"Returns True if this geometry intersects with the other."
return self._topology(capi.ogr_intersects, other)
-
+
def equals(self, other):
"Returns True if this geometry is equivalent to the other."
return self._topology(capi.ogr_equals, other)
@@ -436,7 +461,7 @@ class OGRGeometry(GDALBase):
@property
def convex_hull(self):
"""
- Returns the smallest convex Polygon that contains all the points in
+ Returns the smallest convex Polygon that contains all the points in
this Geometry.
"""
return self._geomgen(capi.geom_convex_hull)
@@ -456,7 +481,7 @@ class OGRGeometry(GDALBase):
return self._geomgen(capi.geom_intersection, other)
def sym_difference(self, other):
- """
+ """
Returns a new geometry which is the symmetric difference of this
geometry and the other.
"""
@@ -545,7 +570,7 @@ class LineString(OGRGeometry):
def y(self):
"Returns the Y coordinates in a list."
return self._listarr(capi.gety)
-
+
@property
def z(self):
"Returns the Z coordinates in a list."
@@ -610,7 +635,7 @@ class GeometryCollection(OGRGeometry):
raise OGRIndexError('index out of range: %s' % index)
else:
return OGRGeometry(capi.clone_geom(capi.get_geom_ref(self.ptr, index)), self.srs)
-
+
def __iter__(self):
"Iterates over each Geometry."
for i in xrange(self.geom_count):
@@ -658,5 +683,5 @@ GEO_CLASSES = {1 : Point,
5 : MultiLineString,
6 : MultiPolygon,
7 : GeometryCollection,
- 101: LinearRing,
+ 101: LinearRing,
}
diff --git a/django/contrib/gis/gdal/prototypes/geom.py b/django/contrib/gis/gdal/prototypes/geom.py
index 2898198d0d..e40f33e745 100644
--- a/django/contrib/gis/gdal/prototypes/geom.py
+++ b/django/contrib/gis/gdal/prototypes/geom.py
@@ -83,7 +83,8 @@ get_geom_srs = srs_output(lgdal.OGR_G_GetSpatialReference, [c_void_p])
get_area = double_output(lgdal.OGR_G_GetArea, [c_void_p])
get_centroid = void_output(lgdal.OGR_G_Centroid, [c_void_p, c_void_p])
get_dims = int_output(lgdal.OGR_G_GetDimension, [c_void_p])
-get_coord_dims = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p])
+get_coord_dim = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p])
+set_coord_dim = void_output(lgdal.OGR_G_SetCoordinateDimension, [c_void_p, c_int], errcheck=False)
get_geom_count = int_output(lgdal.OGR_G_GetGeometryCount, [c_void_p])
get_geom_name = const_string_output(lgdal.OGR_G_GetGeometryName, [c_void_p])
diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py
index c920adc6c0..b5d8046c0e 100644
--- a/django/contrib/gis/gdal/tests/test_geom.py
+++ b/django/contrib/gis/gdal/tests/test_geom.py
@@ -319,6 +319,18 @@ class OGRGeomTest(unittest.TestCase):
self.assertAlmostEqual(trans.x, p.x, prec)
self.assertAlmostEqual(trans.y, p.y, prec)
+ def test09c_transform_dim(self):
+ "Testing coordinate dimension is the same on transformed geometries."
+ ls_orig = OGRGeometry('LINESTRING(-104.609 38.255)', 4326)
+ ls_trans = OGRGeometry('LINESTRING(992385.4472045 481455.4944650)', 2774)
+
+ prec = 3
+ ls_orig.transform(ls_trans.srs)
+ # Making sure the coordinate dimension is still 2D.
+ self.assertEqual(2, ls_orig.coord_dim)
+ self.assertAlmostEqual(ls_trans.x[0], ls_orig.x[0], prec)
+ self.assertAlmostEqual(ls_trans.y[0], ls_orig.y[0], prec)
+
def test10_difference(self):
"Testing difference()."
for i in xrange(len(topology_geoms)):
diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py
index 043dace769..e2500308bb 100644
--- a/django/contrib/gis/tests/geoapp/test_regress.py
+++ b/django/contrib/gis/tests/geoapp/test_regress.py
@@ -28,6 +28,7 @@ class GeoRegressionTests(unittest.TestCase):
kmz = render_to_kmz('gis/kml/placemarks.kml', {'places' : places})
@no_spatialite
+ @no_mysql
def test03_extent(self):
"Testing `extent` on a table with a single point, see #11827."
pnt = City.objects.get(name='Pueblo').point
diff --git a/django/contrib/gis/tests/layermap/models.py b/django/contrib/gis/tests/layermap/models.py
index 5dbd528030..3a34d16f3f 100644
--- a/django/contrib/gis/tests/layermap/models.py
+++ b/django/contrib/gis/tests/layermap/models.py
@@ -29,6 +29,20 @@ class Interstate(models.Model):
path = models.LineStringField()
objects = models.GeoManager()
+# Same as `City` above, but for testing model inheritance.
+class CityBase(models.Model):
+ name = models.CharField(max_length=25)
+ population = models.IntegerField()
+ density = models.DecimalField(max_digits=7, decimal_places=1)
+ point = models.PointField()
+ objects = models.GeoManager()
+
+class ICity1(CityBase):
+ dt = models.DateField()
+
+class ICity2(ICity1):
+ dt_time = models.DateTimeField(auto_now=True)
+
# Mapping dictionaries for the models above.
co_mapping = {'name' : 'Name',
'state' : {'name' : 'State'}, # ForeignKey's use another mapping dictionary for the _related_ Model (State in this case).
diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py
index 56a9f2bdfe..b17e7b92fc 100644
--- a/django/contrib/gis/tests/layermap/tests.py
+++ b/django/contrib/gis/tests/layermap/tests.py
@@ -1,7 +1,7 @@
import os, unittest
from copy import copy
from decimal import Decimal
-from models import City, County, CountyFeat, Interstate, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping
+from models import City, County, CountyFeat, Interstate, ICity1, ICity2, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping
from django.contrib.gis.db.backend import SpatialBackend
from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey
from django.contrib.gis.gdal import DataSource
@@ -242,6 +242,26 @@ class LayerMapTest(unittest.TestCase):
lm.save(step=st, strict=True)
self.county_helper(county_feat=False)
+ def test06_model_inheritance(self):
+ "Tests LayerMapping on inherited models. See #12093."
+ icity_mapping = {'name' : 'Name',
+ 'population' : 'Population',
+ 'density' : 'Density',
+ 'point' : 'POINT',
+ 'dt' : 'Created',
+ }
+
+ # Parent model has geometry field.
+ lm1 = LayerMapping(ICity1, city_shp, icity_mapping)
+ lm1.save()
+
+ # Grandparent has geometry field.
+ lm2 = LayerMapping(ICity2, city_shp, icity_mapping)
+ lm2.save()
+
+ self.assertEqual(6, ICity1.objects.count())
+ self.assertEqual(3, ICity2.objects.count())
+
def suite():
s = unittest.TestSuite()
s.addTest(unittest.makeSuite(LayerMapTest))
diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py
index 43bc70a0ec..57c957811d 100644
--- a/django/contrib/gis/utils/layermapping.py
+++ b/django/contrib/gis/utils/layermapping.py
@@ -514,16 +514,26 @@ class LayerMapping(object):
def geometry_column(self):
"Returns the GeometryColumn model associated with the geographic column."
from django.contrib.gis.models import GeometryColumns
- # Getting the GeometryColumn object.
+ # Use the `get_field_by_name` on the model's options so that we
+ # get the correct model if there's model inheritance -- otherwise
+ # the returned model is None.
+ opts = self.model._meta
+ fld, model, direct, m2m = opts.get_field_by_name(self.geom_field)
+ if model is None: model = self.model
+
+ # Trying to get the `GeometryColumns` object that corresponds to the
+ # the geometry field.
try:
- db_table = self.model._meta.db_table
- geo_col = self.geom_field
+ db_table = model._meta.db_table
+ geo_col = fld.column
+
if SpatialBackend.oracle:
# Making upper case for Oracle.
db_table = db_table.upper()
geo_col = geo_col.upper()
- gc_kwargs = {GeometryColumns.table_name_col() : db_table,
- GeometryColumns.geom_col_name() : geo_col,
+
+ gc_kwargs = { GeometryColumns.table_name_col() : db_table,
+ GeometryColumns.geom_col_name() : geo_col,
}
return GeometryColumns.objects.get(**gc_kwargs)
except Exception, msg:
diff --git a/django/core/context_processors.py b/django/core/context_processors.py
index cb07125ce7..b950dba0f6 100644
--- a/django/core/context_processors.py
+++ b/django/core/context_processors.py
@@ -8,6 +8,8 @@ RequestContext.
"""
from django.conf import settings
+from django.middleware.csrf import get_token
+from django.utils.functional import lazy, memoize, SimpleLazyObject
def auth(request):
"""
@@ -17,17 +19,46 @@ def auth(request):
If there is no 'user' attribute in the request, uses AnonymousUser (from
django.contrib.auth).
"""
- if hasattr(request, 'user'):
- user = request.user
- else:
- from django.contrib.auth.models import AnonymousUser
- user = AnonymousUser()
+ # If we access request.user, request.session is accessed, which results in
+ # 'Vary: Cookie' being sent in every request that uses this context
+ # processor, which can easily be every request on a site if
+ # TEMPLATE_CONTEXT_PROCESSORS has this context processor added. This kills
+ # the ability to cache. So, we carefully ensure these attributes are lazy.
+ # We don't use django.utils.functional.lazy() for User, because that
+ # requires knowing the class of the object we want to proxy, which could
+ # break with custom auth backends. LazyObject is a less complete but more
+ # flexible solution that is a good enough wrapper for 'User'.
+ def get_user():
+ if hasattr(request, 'user'):
+ return request.user
+ else:
+ from django.contrib.auth.models import AnonymousUser
+ return AnonymousUser()
+
return {
- 'user': user,
- 'messages': user.get_and_delete_messages(),
- 'perms': PermWrapper(user),
+ 'user': SimpleLazyObject(get_user),
+ 'messages': lazy(memoize(lambda: get_user().get_and_delete_messages(), {}, 0), list)(),
+ 'perms': lazy(lambda: PermWrapper(get_user()), PermWrapper)(),
}
+def csrf(request):
+ """
+ Context processor that provides a CSRF token, or the string 'NOTPROVIDED' if
+ it has not been provided by either a view decorator or the middleware
+ """
+ def _get_val():
+ token = get_token(request)
+ if token is None:
+ # In order to be able to provide debugging info in the
+ # case of misconfiguration, we use a sentinel value
+ # instead of returning an empty dict.
+ return 'NOTPROVIDED'
+ else:
+ return token
+ _get_val = lazy(_get_val, str)
+
+ return {'csrf_token': _get_val() }
+
def debug(request):
"Returns context variables helpful for debugging."
context_extras = {}
@@ -79,7 +110,7 @@ class PermWrapper(object):
def __getitem__(self, module_name):
return PermLookupDict(self.user, module_name)
-
+
def __iter__(self):
# I am large, I contain multitudes.
raise TypeError("PermWrapper is not iterable.")
diff --git a/django/core/files/storage.py b/django/core/files/storage.py
index e183c17dd3..96c0b54623 100644
--- a/django/core/files/storage.py
+++ b/django/core/files/storage.py
@@ -118,10 +118,6 @@ class Storage(object):
"""
raise NotImplementedError()
- # Needed by django.utils.functional.LazyObject (via DefaultStorage).
- def get_all_members(self):
- return self.__members__
-
class FileSystemStorage(Storage):
"""
Standard filesystem storage
diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py
new file mode 100644
index 0000000000..b02575793d
--- /dev/null
+++ b/django/core/mail/__init__.py
@@ -0,0 +1,110 @@
+"""
+Tools for sending email.
+"""
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
+# Imported for backwards compatibility, and for the sake
+# of a cleaner namespace. These symbols used to be in
+# django/core/mail.py before the introduction of email
+# backends and the subsequent reorganization (See #10355)
+from django.core.mail.utils import CachedDnsName, DNS_NAME
+from django.core.mail.message import \
+ EmailMessage, EmailMultiAlternatives, \
+ SafeMIMEText, SafeMIMEMultipart, \
+ DEFAULT_ATTACHMENT_MIME_TYPE, make_msgid, \
+ BadHeaderError, forbid_multi_line_headers
+from django.core.mail.backends.smtp import EmailBackend as _SMTPConnection
+
+def get_connection(backend=None, fail_silently=False, **kwds):
+ """Load an e-mail backend and return an instance of it.
+
+ If backend is None (default) settings.EMAIL_BACKEND is used.
+
+ Both fail_silently and other keyword arguments are used in the
+ constructor of the backend.
+ """
+ path = backend or settings.EMAIL_BACKEND
+ try:
+ mod = import_module(path)
+ except ImportError, e:
+ raise ImproperlyConfigured(('Error importing email backend %s: "%s"'
+ % (path, e)))
+ try:
+ cls = getattr(mod, 'EmailBackend')
+ except AttributeError:
+ raise ImproperlyConfigured(('Module "%s" does not define a '
+ '"EmailBackend" class' % path))
+ return cls(fail_silently=fail_silently, **kwds)
+
+
+def send_mail(subject, message, from_email, recipient_list,
+ fail_silently=False, auth_user=None, auth_password=None,
+ connection=None):
+ """
+ Easy wrapper for sending a single message to a recipient list. All members
+ of the recipient list will see the other recipients in the 'To' field.
+
+ If auth_user is None, the EMAIL_HOST_USER setting is used.
+ If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
+
+ Note: The API for this method is frozen. New code wanting to extend the
+ functionality should use the EmailMessage class directly.
+ """
+ connection = connection or get_connection(username=auth_user,
+ password=auth_password,
+ fail_silently=fail_silently)
+ return EmailMessage(subject, message, from_email, recipient_list,
+ connection=connection).send()
+
+
+def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
+ auth_password=None, connection=None):
+ """
+ Given a datatuple of (subject, message, from_email, recipient_list), sends
+ each message to each recipient list. Returns the number of e-mails sent.
+
+ If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
+ If auth_user and auth_password are set, they're used to log in.
+ If auth_user is None, the EMAIL_HOST_USER setting is used.
+ If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
+
+ Note: The API for this method is frozen. New code wanting to extend the
+ functionality should use the EmailMessage class directly.
+ """
+ connection = connection or get_connection(username=auth_user,
+ password=auth_password,
+ fail_silently=fail_silently)
+ messages = [EmailMessage(subject, message, sender, recipient)
+ for subject, message, sender, recipient in datatuple]
+ return connection.send_messages(messages)
+
+
+def mail_admins(subject, message, fail_silently=False, connection=None):
+ """Sends a message to the admins, as defined by the ADMINS setting."""
+ if not settings.ADMINS:
+ return
+ EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
+ settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
+ connection=connection).send(fail_silently=fail_silently)
+
+
+def mail_managers(subject, message, fail_silently=False, connection=None):
+ """Sends a message to the managers, as defined by the MANAGERS setting."""
+ if not settings.MANAGERS:
+ return
+ EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
+ settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS],
+ connection=connection).send(fail_silently=fail_silently)
+
+
+class SMTPConnection(_SMTPConnection):
+ def __init__(self, *args, **kwds):
+ import warnings
+ warnings.warn(
+ 'mail.SMTPConnection is deprecated; use mail.get_connection() instead.',
+ DeprecationWarning
+ )
+ super(SMTPConnection, self).__init__(*args, **kwds)
diff --git a/django/core/mail/backends/__init__.py b/django/core/mail/backends/__init__.py
new file mode 100644
index 0000000000..5973b499b0
--- /dev/null
+++ b/django/core/mail/backends/__init__.py
@@ -0,0 +1 @@
+# Mail backends shipped with Django.
diff --git a/django/core/mail/backends/base.py b/django/core/mail/backends/base.py
new file mode 100644
index 0000000000..9a3092849d
--- /dev/null
+++ b/django/core/mail/backends/base.py
@@ -0,0 +1,39 @@
+"""Base email backend class."""
+
+class BaseEmailBackend(object):
+ """
+ Base class for email backend implementations.
+
+ Subclasses must at least overwrite send_messages().
+ """
+ def __init__(self, fail_silently=False, **kwargs):
+ self.fail_silently = fail_silently
+
+ def open(self):
+ """Open a network connection.
+
+ This method can be overwritten by backend implementations to
+ open a network connection.
+
+ It's up to the backend implementation to track the status of
+ a network connection if it's needed by the backend.
+
+ This method can be called by applications to force a single
+ network connection to be used when sending mails. See the
+ send_messages() method of the SMTP backend for a reference
+ implementation.
+
+ The default implementation does nothing.
+ """
+ pass
+
+ def close(self):
+ """Close a network connection."""
+ pass
+
+ def send_messages(self, email_messages):
+ """
+ Sends one or more EmailMessage objects and returns the number of email
+ messages sent.
+ """
+ raise NotImplementedError
diff --git a/django/core/mail/backends/console.py b/django/core/mail/backends/console.py
new file mode 100644
index 0000000000..fa71f3816f
--- /dev/null
+++ b/django/core/mail/backends/console.py
@@ -0,0 +1,37 @@
+"""
+Email backend that writes messages to console instead of sending them.
+"""
+import sys
+import threading
+
+from django.core.mail.backends.base import BaseEmailBackend
+
+class EmailBackend(BaseEmailBackend):
+ def __init__(self, *args, **kwargs):
+ self.stream = kwargs.pop('stream', sys.stdout)
+ self._lock = threading.RLock()
+ super(EmailBackend, self).__init__(*args, **kwargs)
+
+ def send_messages(self, email_messages):
+ """Write all messages to the stream in a thread-safe way."""
+ if not email_messages:
+ return
+ self._lock.acquire()
+ try:
+ # The try-except is nested to allow for
+ # Python 2.4 support (Refs #12147)
+ try:
+ stream_created = self.open()
+ for message in email_messages:
+ self.stream.write('%s\n' % message.message().as_string())
+ self.stream.write('-'*79)
+ self.stream.write('\n')
+ self.stream.flush() # flush after each message
+ if stream_created:
+ self.close()
+ except:
+ if not self.fail_silently:
+ raise
+ finally:
+ self._lock.release()
+ return len(email_messages)
diff --git a/django/core/mail/backends/dummy.py b/django/core/mail/backends/dummy.py
new file mode 100644
index 0000000000..273aa0d88e
--- /dev/null
+++ b/django/core/mail/backends/dummy.py
@@ -0,0 +1,9 @@
+"""
+Dummy email backend that does nothing.
+"""
+
+from django.core.mail.backends.base import BaseEmailBackend
+
+class EmailBackend(BaseEmailBackend):
+ def send_messages(self, email_messages):
+ return len(email_messages)
diff --git a/django/core/mail/backends/filebased.py b/django/core/mail/backends/filebased.py
new file mode 100644
index 0000000000..3f6b99b057
--- /dev/null
+++ b/django/core/mail/backends/filebased.py
@@ -0,0 +1,59 @@
+"""Email backend that writes messages to a file."""
+
+import datetime
+import os
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.core.mail.backends.console import EmailBackend as ConsoleEmailBackend
+
+class EmailBackend(ConsoleEmailBackend):
+ def __init__(self, *args, **kwargs):
+ self._fname = None
+ if 'file_path' in kwargs:
+ self.file_path = kwargs.pop('file_path')
+ else:
+ self.file_path = getattr(settings, 'EMAIL_FILE_PATH',None)
+ # Make sure self.file_path is a string.
+ if not isinstance(self.file_path, basestring):
+ raise ImproperlyConfigured('Path for saving emails is invalid: %r' % self.file_path)
+ self.file_path = os.path.abspath(self.file_path)
+ # Make sure that self.file_path is an directory if it exists.
+ if os.path.exists(self.file_path) and not os.path.isdir(self.file_path):
+ raise ImproperlyConfigured('Path for saving email messages exists, but is not a directory: %s' % self.file_path)
+ # Try to create it, if it not exists.
+ elif not os.path.exists(self.file_path):
+ try:
+ os.makedirs(self.file_path)
+ except OSError, err:
+ raise ImproperlyConfigured('Could not create directory for saving email messages: %s (%s)' % (self.file_path, err))
+ # Make sure that self.file_path is writable.
+ if not os.access(self.file_path, os.W_OK):
+ raise ImproperlyConfigured('Could not write to directory: %s' % self.file_path)
+ # Finally, call super().
+ # Since we're using the console-based backend as a base,
+ # force the stream to be None, so we don't default to stdout
+ kwargs['stream'] = None
+ super(EmailBackend, self).__init__(*args, **kwargs)
+
+ def _get_filename(self):
+ """Return a unique file name."""
+ if self._fname is None:
+ timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
+ fname = "%s-%s.log" % (timestamp, abs(id(self)))
+ self._fname = os.path.join(self.file_path, fname)
+ return self._fname
+
+ def open(self):
+ if self.stream is None:
+ self.stream = open(self._get_filename(), 'a')
+ return True
+ return False
+
+ def close(self):
+ try:
+ if self.stream is not None:
+ self.stream.close()
+ finally:
+ self.stream = None
+
diff --git a/django/core/mail/backends/locmem.py b/django/core/mail/backends/locmem.py
new file mode 100644
index 0000000000..642bfc49fb
--- /dev/null
+++ b/django/core/mail/backends/locmem.py
@@ -0,0 +1,24 @@
+"""
+Backend for test environment.
+"""
+
+from django.core import mail
+from django.core.mail.backends.base import BaseEmailBackend
+
+class EmailBackend(BaseEmailBackend):
+ """A email backend for use during test sessions.
+
+ The test connection stores email messages in a dummy outbox,
+ rather than sending them out on the wire.
+
+ The dummy outbox is accessible through the outbox instance attribute.
+ """
+ def __init__(self, *args, **kwargs):
+ super(EmailBackend, self).__init__(*args, **kwargs)
+ if not hasattr(mail, 'outbox'):
+ mail.outbox = []
+
+ def send_messages(self, messages):
+ """Redirect messages to the dummy outbox"""
+ mail.outbox.extend(messages)
+ return len(messages)
diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py
new file mode 100644
index 0000000000..63efe438d3
--- /dev/null
+++ b/django/core/mail/backends/smtp.py
@@ -0,0 +1,106 @@
+"""SMTP email backend class."""
+
+import smtplib
+import socket
+import threading
+
+from django.conf import settings
+from django.core.mail.backends.base import BaseEmailBackend
+from django.core.mail.utils import DNS_NAME
+
+class EmailBackend(BaseEmailBackend):
+ """
+ A wrapper that manages the SMTP network connection.
+ """
+ def __init__(self, host=None, port=None, username=None, password=None,
+ use_tls=None, fail_silently=False, **kwargs):
+ super(EmailBackend, self).__init__(fail_silently=fail_silently)
+ self.host = host or settings.EMAIL_HOST
+ self.port = port or settings.EMAIL_PORT
+ self.username = username or settings.EMAIL_HOST_USER
+ self.password = password or settings.EMAIL_HOST_PASSWORD
+ if use_tls is None:
+ self.use_tls = settings.EMAIL_USE_TLS
+ else:
+ self.use_tls = use_tls
+ self.connection = None
+ self._lock = threading.RLock()
+
+ def open(self):
+ """
+ Ensures we have a connection to the email server. Returns whether or
+ not a new connection was required (True or False).
+ """
+ if self.connection:
+ # Nothing to do if the connection is already open.
+ return False
+ try:
+ # If local_hostname is not specified, socket.getfqdn() gets used.
+ # For performance, we use the cached FQDN for local_hostname.
+ self.connection = smtplib.SMTP(self.host, self.port,
+ local_hostname=DNS_NAME.get_fqdn())
+ if self.use_tls:
+ self.connection.ehlo()
+ self.connection.starttls()
+ self.connection.ehlo()
+ if self.username and self.password:
+ self.connection.login(self.username, self.password)
+ return True
+ except:
+ if not self.fail_silently:
+ raise
+
+ def close(self):
+ """Closes the connection to the email server."""
+ try:
+ try:
+ self.connection.quit()
+ except socket.sslerror:
+ # This happens when calling quit() on a TLS connection
+ # sometimes.
+ self.connection.close()
+ except:
+ if self.fail_silently:
+ return
+ raise
+ finally:
+ self.connection = None
+
+ def send_messages(self, email_messages):
+ """
+ Sends one or more EmailMessage objects and returns the number of email
+ messages sent.
+ """
+ if not email_messages:
+ return
+ self._lock.acquire()
+ try:
+ new_conn_created = self.open()
+ if not self.connection:
+ # We failed silently on open().
+ # Trying to send would be pointless.
+ return
+ num_sent = 0
+ for message in email_messages:
+ sent = self._send(message)
+ if sent:
+ num_sent += 1
+ if new_conn_created:
+ self.close()
+ finally:
+ self._lock.release()
+ return num_sent
+
+ def _send(self, email_message):
+ """A helper method that does the actual sending."""
+ if not email_message.recipients():
+ return False
+ try:
+ self.connection.sendmail(email_message.from_email,
+ email_message.recipients(),
+ email_message.message().as_string())
+ except:
+ if not self.fail_silently:
+ raise
+ return False
+ return True
diff --git a/django/core/mail.py b/django/core/mail/message.py
index c305699158..14d0017311 100644
--- a/django/core/mail.py
+++ b/django/core/mail/message.py
@@ -1,21 +1,16 @@
-"""
-Tools for sending email.
-"""
-
import mimetypes
import os
-import smtplib
-import socket
-import time
import random
+import time
from email import Charset, Encoders
from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.Header import Header
-from email.Utils import formatdate, parseaddr, formataddr
+from email.Utils import formatdate, getaddresses, formataddr
from django.conf import settings
+from django.core.mail.utils import DNS_NAME
from django.utils.encoding import smart_str, force_unicode
# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
@@ -26,18 +21,10 @@ Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
# and cannot be guessed).
DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
-# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
-# seconds, which slows down the restart of the server.
-class CachedDnsName(object):
- def __str__(self):
- return self.get_fqdn()
- def get_fqdn(self):
- if not hasattr(self, '_fqdn'):
- self._fqdn = socket.getfqdn()
- return self._fqdn
+class BadHeaderError(ValueError):
+ pass
-DNS_NAME = CachedDnsName()
# Copied from Python standard library, with the following modifications:
# * Used cached hostname for performance.
@@ -66,8 +53,6 @@ def make_msgid(idstring=None):
msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
return msgid
-class BadHeaderError(ValueError):
- pass
def forbid_multi_line_headers(name, val):
"""Forbids multi-line headers, to prevent header injection."""
@@ -79,8 +64,7 @@ def forbid_multi_line_headers(name, val):
except UnicodeEncodeError:
if name.lower() in ('to', 'from', 'cc'):
result = []
- for item in val.split(', '):
- nm, addr = parseaddr(item)
+ for nm, addr in getaddresses((val,)):
nm = str(Header(nm, settings.DEFAULT_CHARSET))
result.append(formataddr((nm, str(addr))))
val = ', '.join(result)
@@ -91,104 +75,18 @@ def forbid_multi_line_headers(name, val):
val = Header(val)
return name, val
+
class SafeMIMEText(MIMEText):
def __setitem__(self, name, val):
name, val = forbid_multi_line_headers(name, val)
MIMEText.__setitem__(self, name, val)
+
class SafeMIMEMultipart(MIMEMultipart):
def __setitem__(self, name, val):
name, val = forbid_multi_line_headers(name, val)
MIMEMultipart.__setitem__(self, name, val)
-class SMTPConnection(object):
- """
- A wrapper that manages the SMTP network connection.
- """
-
- def __init__(self, host=None, port=None, username=None, password=None,
- use_tls=None, fail_silently=False):
- self.host = host or settings.EMAIL_HOST
- self.port = port or settings.EMAIL_PORT
- self.username = username or settings.EMAIL_HOST_USER
- self.password = password or settings.EMAIL_HOST_PASSWORD
- self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS
- self.fail_silently = fail_silently
- self.connection = None
-
- def open(self):
- """
- Ensures we have a connection to the email server. Returns whether or
- not a new connection was required (True or False).
- """
- if self.connection:
- # Nothing to do if the connection is already open.
- return False
- try:
- # If local_hostname is not specified, socket.getfqdn() gets used.
- # For performance, we use the cached FQDN for local_hostname.
- self.connection = smtplib.SMTP(self.host, self.port,
- local_hostname=DNS_NAME.get_fqdn())
- if self.use_tls:
- self.connection.ehlo()
- self.connection.starttls()
- self.connection.ehlo()
- if self.username and self.password:
- self.connection.login(self.username, self.password)
- return True
- except:
- if not self.fail_silently:
- raise
-
- def close(self):
- """Closes the connection to the email server."""
- try:
- try:
- self.connection.quit()
- except socket.sslerror:
- # This happens when calling quit() on a TLS connection
- # sometimes.
- self.connection.close()
- except:
- if self.fail_silently:
- return
- raise
- finally:
- self.connection = None
-
- def send_messages(self, email_messages):
- """
- Sends one or more EmailMessage objects and returns the number of email
- messages sent.
- """
- if not email_messages:
- return
- new_conn_created = self.open()
- if not self.connection:
- # We failed silently on open(). Trying to send would be pointless.
- return
- num_sent = 0
- for message in email_messages:
- sent = self._send(message)
- if sent:
- num_sent += 1
- if new_conn_created:
- self.close()
- return num_sent
-
- def _send(self, email_message):
- """A helper method that does the actual sending."""
- if not email_message.recipients():
- return False
- try:
- self.connection.sendmail(email_message.from_email,
- email_message.recipients(),
- email_message.message().as_string())
- except:
- if not self.fail_silently:
- raise
- return False
- return True
class EmailMessage(object):
"""
@@ -199,14 +97,14 @@ class EmailMessage(object):
encoding = None # None => use settings default
def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
- connection=None, attachments=None, headers=None):
+ connection=None, attachments=None, headers=None):
"""
Initialize a single email message (which can be sent to multiple
recipients).
- All strings used to create the message can be unicode strings (or UTF-8
- bytestrings). The SafeMIMEText class will handle any necessary encoding
- conversions.
+ All strings used to create the message can be unicode strings
+ (or UTF-8 bytestrings). The SafeMIMEText class will handle any
+ necessary encoding conversions.
"""
if to:
assert not isinstance(to, basestring), '"to" argument must be a list or tuple'
@@ -226,8 +124,9 @@ class EmailMessage(object):
self.connection = connection
def get_connection(self, fail_silently=False):
+ from django.core.mail import get_connection
if not self.connection:
- self.connection = SMTPConnection(fail_silently=fail_silently)
+ self.connection = get_connection(fail_silently=fail_silently)
return self.connection
def message(self):
@@ -332,6 +231,7 @@ class EmailMessage(object):
filename=filename)
return attachment
+
class EmailMultiAlternatives(EmailMessage):
"""
A version of EmailMessage that makes it easy to send multipart/alternative
@@ -371,56 +271,3 @@ class EmailMultiAlternatives(EmailMessage):
for alternative in self.alternatives:
msg.attach(self._create_mime_attachment(*alternative))
return msg
-
-def send_mail(subject, message, from_email, recipient_list,
- fail_silently=False, auth_user=None, auth_password=None):
- """
- Easy wrapper for sending a single message to a recipient list. All members
- of the recipient list will see the other recipients in the 'To' field.
-
- If auth_user is None, the EMAIL_HOST_USER setting is used.
- If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
-
- Note: The API for this method is frozen. New code wanting to extend the
- functionality should use the EmailMessage class directly.
- """
- connection = SMTPConnection(username=auth_user, password=auth_password,
- fail_silently=fail_silently)
- return EmailMessage(subject, message, from_email, recipient_list,
- connection=connection).send()
-
-def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
- auth_password=None):
- """
- Given a datatuple of (subject, message, from_email, recipient_list), sends
- each message to each recipient list. Returns the number of e-mails sent.
-
- If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
- If auth_user and auth_password are set, they're used to log in.
- If auth_user is None, the EMAIL_HOST_USER setting is used.
- If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
-
- Note: The API for this method is frozen. New code wanting to extend the
- functionality should use the EmailMessage class directly.
- """
- connection = SMTPConnection(username=auth_user, password=auth_password,
- fail_silently=fail_silently)
- messages = [EmailMessage(subject, message, sender, recipient)
- for subject, message, sender, recipient in datatuple]
- return connection.send_messages(messages)
-
-def mail_admins(subject, message, fail_silently=False):
- """Sends a message to the admins, as defined by the ADMINS setting."""
- if not settings.ADMINS:
- return
- EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
- settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS]
- ).send(fail_silently=fail_silently)
-
-def mail_managers(subject, message, fail_silently=False):
- """Sends a message to the managers, as defined by the MANAGERS setting."""
- if not settings.MANAGERS:
- return
- EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
- settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]
- ).send(fail_silently=fail_silently)
diff --git a/django/core/mail/utils.py b/django/core/mail/utils.py
new file mode 100644
index 0000000000..322a3a1b79
--- /dev/null
+++ b/django/core/mail/utils.py
@@ -0,0 +1,19 @@
+"""
+Email message and email sending related helper functions.
+"""
+
+import socket
+
+
+# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
+# seconds, which slows down the restart of the server.
+class CachedDnsName(object):
+ def __str__(self):
+ return self.get_fqdn()
+
+ def get_fqdn(self):
+ if not hasattr(self, '_fqdn'):
+ self._fqdn = socket.getfqdn()
+ return self._fqdn
+
+DNS_NAME = CachedDnsName()
diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py
index fe51d45bb3..165006efd1 100644
--- a/django/core/management/commands/syncdb.py
+++ b/django/core/management/commands/syncdb.py
@@ -57,12 +57,15 @@ class Command(NoArgsCommand):
# Create the tables for each model
for app in models.get_apps():
app_name = app.__name__.split('.')[-2]
- model_list = models.get_models(app)
+ model_list = models.get_models(app, include_auto_created=True)
for model in model_list:
# Create the model's database table, if it doesn't already exist.
if verbosity >= 2:
print "Processing %s.%s model" % (app_name, model._meta.object_name)
- if connection.introspection.table_name_converter(model._meta.db_table) in tables:
+ opts = model._meta
+ if (connection.introspection.table_name_converter(opts.db_table) in tables or
+ (opts.auto_created and
+ connection.introspection.table_name_converter(opts.auto_created._meta.db_table in tables))):
continue
sql, references = connection.creation.sql_create_model(model, self.style, seen_models)
seen_models.add(model)
@@ -78,19 +81,6 @@ class Command(NoArgsCommand):
cursor.execute(statement)
tables.append(connection.introspection.table_name_converter(model._meta.db_table))
- # Create the m2m tables. This must be done after all tables have been created
- # to ensure that all referred tables will exist.
- for app in models.get_apps():
- app_name = app.__name__.split('.')[-2]
- model_list = models.get_models(app)
- for model in model_list:
- if model in created_models:
- sql = connection.creation.sql_for_many_to_many(model, self.style)
- if sql:
- if verbosity >= 2:
- print "Creating many-to-many tables for %s.%s model" % (app_name, model._meta.object_name)
- for statement in sql:
- cursor.execute(statement)
transaction.commit_unless_managed()
diff --git a/django/core/management/sql.py b/django/core/management/sql.py
index 14fd3f8214..caf40d088d 100644
--- a/django/core/management/sql.py
+++ b/django/core/management/sql.py
@@ -23,7 +23,7 @@ def sql_create(app, style):
# We trim models from the current app so that the sqlreset command does not
# generate invalid SQL (leaving models out of known_models is harmless, so
# we can be conservative).
- app_models = models.get_models(app)
+ app_models = models.get_models(app, include_auto_created=True)
final_output = []
tables = connection.introspection.table_names()
known_models = set([model for model in connection.introspection.installed_models(tables) if model not in app_models])
@@ -40,10 +40,6 @@ def sql_create(app, style):
# Keep track of the fact that we've created the table for this model.
known_models.add(model)
- # Create the many-to-many join tables.
- for model in app_models:
- final_output.extend(connection.creation.sql_for_many_to_many(model, style))
-
# Handle references to tables that are from other apps
# but don't exist physically.
not_installed_models = set(pending_references.keys())
@@ -82,7 +78,7 @@ def sql_delete(app, style):
to_delete = set()
references_to_delete = {}
- app_models = models.get_models(app)
+ app_models = models.get_models(app, include_auto_created=True)
for model in app_models:
if cursor and connection.introspection.table_name_converter(model._meta.db_table) in table_names:
# The table exists, so it needs to be dropped
@@ -97,13 +93,6 @@ def sql_delete(app, style):
if connection.introspection.table_name_converter(model._meta.db_table) in table_names:
output.extend(connection.creation.sql_destroy_model(model, references_to_delete, style))
- # Output DROP TABLE statements for many-to-many tables.
- for model in app_models:
- opts = model._meta
- for f in opts.local_many_to_many:
- if cursor and connection.introspection.table_name_converter(f.m2m_db_table()) in table_names:
- output.extend(connection.creation.sql_destroy_many_to_many(model, f, style))
-
# Close database connection explicitly, in case this output is being piped
# directly into a database client, to avoid locking issues.
if cursor:
diff --git a/django/core/management/validation.py b/django/core/management/validation.py
index b971558ac7..97164d75c3 100644
--- a/django/core/management/validation.py
+++ b/django/core/management/validation.py
@@ -79,27 +79,28 @@ def get_validation_errors(outfile, app=None):
rel_opts = f.rel.to._meta
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
rel_query_name = f.related_query_name()
- for r in rel_opts.fields:
- if r.name == rel_name:
- e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
- if r.name == rel_query_name:
- e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
- for r in rel_opts.local_many_to_many:
- if r.name == rel_name:
- e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
- if r.name == rel_query_name:
- e.add(opts, "Reverse query name for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
- for r in rel_opts.get_all_related_many_to_many_objects():
- if r.get_accessor_name() == rel_name:
- e.add(opts, "Accessor for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
- if r.get_accessor_name() == rel_query_name:
- e.add(opts, "Reverse query name for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
- for r in rel_opts.get_all_related_objects():
- if r.field is not f:
+ if not f.rel.is_hidden():
+ for r in rel_opts.fields:
+ if r.name == rel_name:
+ e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
+ if r.name == rel_query_name:
+ e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
+ for r in rel_opts.local_many_to_many:
+ if r.name == rel_name:
+ e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
+ if r.name == rel_query_name:
+ e.add(opts, "Reverse query name for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name))
+ for r in rel_opts.get_all_related_many_to_many_objects():
if r.get_accessor_name() == rel_name:
- e.add(opts, "Accessor for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
+ e.add(opts, "Accessor for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
if r.get_accessor_name() == rel_query_name:
- e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
+ e.add(opts, "Reverse query name for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
+ for r in rel_opts.get_all_related_objects():
+ if r.field is not f:
+ if r.get_accessor_name() == rel_name:
+ e.add(opts, "Accessor for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
+ if r.get_accessor_name() == rel_query_name:
+ e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name))
seen_intermediary_signatures = []
for i, f in enumerate(opts.local_many_to_many):
@@ -117,48 +118,80 @@ def get_validation_errors(outfile, app=None):
if f.unique:
e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name)
- if getattr(f.rel, 'through', None) is not None:
- if hasattr(f.rel, 'through_model'):
- from_model, to_model = cls, f.rel.to
- if from_model == to_model and f.rel.symmetrical:
- e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.")
- seen_from, seen_to, seen_self = False, False, 0
- for inter_field in f.rel.through_model._meta.fields:
- rel_to = getattr(inter_field.rel, 'to', None)
- if from_model == to_model: # relation to self
- if rel_to == from_model:
- seen_self += 1
- if seen_self > 2:
- e.add(opts, "Intermediary model %s has more than two foreign keys to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name))
- else:
- if rel_to == from_model:
- if seen_from:
- e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name))
- else:
- seen_from = True
- elif rel_to == to_model:
- if seen_to:
- e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_to._meta.object_name))
- else:
- seen_to = True
- if f.rel.through_model not in models.get_models():
- e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed." % (f.name, f.rel.through))
- signature = (f.rel.to, cls, f.rel.through_model)
- if signature in seen_intermediary_signatures:
- e.add(opts, "The model %s has two manually-defined m2m relations through the model %s, which is not permitted. Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, f.rel.through_model._meta.object_name))
+ if f.rel.through is not None and not isinstance(f.rel.through, basestring):
+ from_model, to_model = cls, f.rel.to
+ if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.auto_created:
+ e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.")
+ seen_from, seen_to, seen_self = False, False, 0
+ for inter_field in f.rel.through._meta.fields:
+ rel_to = getattr(inter_field.rel, 'to', None)
+ if from_model == to_model: # relation to self
+ if rel_to == from_model:
+ seen_self += 1
+ if seen_self > 2:
+ e.add(opts, "Intermediary model %s has more than "
+ "two foreign keys to %s, which is ambiguous "
+ "and is not permitted." % (
+ f.rel.through._meta.object_name,
+ from_model._meta.object_name
+ )
+ )
else:
- seen_intermediary_signatures.append(signature)
- seen_related_fk, seen_this_fk = False, False
- for field in f.rel.through_model._meta.fields:
- if field.rel:
- if not seen_related_fk and field.rel.to == f.rel.to:
- seen_related_fk = True
- elif field.rel.to == cls:
- seen_this_fk = True
- if not seen_related_fk or not seen_this_fk:
- e.add(opts, "'%s' has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name))
+ if rel_to == from_model:
+ if seen_from:
+ e.add(opts, "Intermediary model %s has more "
+ "than one foreign key to %s, which is "
+ "ambiguous and is not permitted." % (
+ f.rel.through._meta.object_name,
+ from_model._meta.object_name
+ )
+ )
+ else:
+ seen_from = True
+ elif rel_to == to_model:
+ if seen_to:
+ e.add(opts, "Intermediary model %s has more "
+ "than one foreign key to %s, which is "
+ "ambiguous and is not permitted." % (
+ f.rel.through._meta.object_name,
+ rel_to._meta.object_name
+ )
+ )
+ else:
+ seen_to = True
+ if f.rel.through not in models.get_models(include_auto_created=True):
+ e.add(opts, "'%s' specifies an m2m relation through model "
+ "%s, which has not been installed." % (f.name, f.rel.through)
+ )
+ signature = (f.rel.to, cls, f.rel.through)
+ if signature in seen_intermediary_signatures:
+ e.add(opts, "The model %s has two manually-defined m2m "
+ "relations through the model %s, which is not "
+ "permitted. Please consider using an extra field on "
+ "your intermediary model instead." % (
+ cls._meta.object_name,
+ f.rel.through._meta.object_name
+ )
+ )
else:
- e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through))
+ seen_intermediary_signatures.append(signature)
+ seen_related_fk, seen_this_fk = False, False
+ for field in f.rel.through._meta.fields:
+ if field.rel:
+ if not seen_related_fk and field.rel.to == f.rel.to:
+ seen_related_fk = True
+ elif field.rel.to == cls:
+ seen_this_fk = True
+ if not seen_related_fk or not seen_this_fk:
+ e.add(opts, "'%s' has a manually-defined m2m relation "
+ "through model %s, which does not have foreign keys "
+ "to %s and %s" % (f.name, f.rel.through._meta.object_name,
+ f.rel.to._meta.object_name, cls._meta.object_name)
+ )
+ elif isinstance(f.rel.through, basestring):
+ e.add(opts, "'%s' specifies an m2m relation through model %s, "
+ "which has not been installed" % (f.name, f.rel.through)
+ )
rel_opts = f.rel.to._meta
rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name()
diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py
index b672e4efc3..7b77804009 100644
--- a/django/core/serializers/python.py
+++ b/django/core/serializers/python.py
@@ -56,7 +56,7 @@ class Serializer(base.Serializer):
self._current[field.name] = smart_unicode(related, strings_only=True)
def handle_m2m_field(self, obj, field):
- if field.creates_table:
+ if field.rel.through._meta.auto_created:
self._current[field.name] = [smart_unicode(related._get_pk_val(), strings_only=True)
for related in getattr(obj, field.name).iterator()]
diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py
index 2d74fe28f3..4cde0b039d 100644
--- a/django/core/serializers/xml_serializer.py
+++ b/django/core/serializers/xml_serializer.py
@@ -98,7 +98,7 @@ class Serializer(base.Serializer):
serialized as references to the object's PK (i.e. the related *data*
is not dumped, just the relation).
"""
- if field.creates_table:
+ if field.rel.through._meta.auto_created:
self._start_relational_field(field)
for relobj in getattr(obj, field.name).iterator():
self.xml.addQuickElement("object", attrs={"pk" : smart_unicode(relobj._get_pk_val())})
@@ -233,4 +233,3 @@ def getInnerText(node):
else:
pass
return u"".join(inner_text)
-
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 6ebe12054f..f8a8f8ae23 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -3,11 +3,6 @@ import types
import sys
import os
from itertools import izip
-try:
- set
-except NameError:
- from sets import Set as set # Python 2.3 fallback.
-
import django.db.models.manager # Imported to register signal handler.
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS
from django.core import validators
@@ -25,7 +20,6 @@ from django.utils.encoding import smart_str, force_unicode, smart_unicode
from django.utils.text import get_text_list, capfirst
from django.conf import settings
-
class ModelBase(type):
"""
Metaclass for all models.
@@ -239,7 +233,6 @@ class ModelBase(type):
signals.class_prepared.send(sender=cls)
-
class Model(object):
__metaclass__ = ModelBase
_deferred = False
@@ -303,7 +296,14 @@ class Model(object):
if rel_obj is None and field.null:
val = None
else:
- val = kwargs.pop(field.attname, field.get_default())
+ try:
+ val = kwargs.pop(field.attname)
+ except KeyError:
+ # This is done with an exception rather than the
+ # default argument on pop because we don't want
+ # get_default() to be evaluated, and then not used.
+ # Refs #12057.
+ val = field.get_default()
else:
val = field.get_default()
if is_related_object:
@@ -355,20 +355,26 @@ class Model(object):
only module-level classes can be pickled by the default path.
"""
data = self.__dict__
- if not self._deferred:
- return (self.__class__, (), data)
+ model = self.__class__
+ # The obvious thing to do here is to invoke super().__reduce__()
+ # for the non-deferred case. Don't do that.
+ # On Python 2.4, there is something wierd with __reduce__,
+ # and as a result, the super call will cause an infinite recursion.
+ # See #10547 and #12121.
defers = []
pk_val = None
- for field in self._meta.fields:
- if isinstance(self.__class__.__dict__.get(field.attname),
- DeferredAttribute):
- defers.append(field.attname)
- if pk_val is None:
- # The pk_val and model values are the same for all
- # DeferredAttribute classes, so we only need to do this
- # once.
- obj = self.__class__.__dict__[field.attname]
- model = obj.model_ref()
+ if self._deferred:
+ for field in self._meta.fields:
+ if isinstance(self.__class__.__dict__.get(field.attname),
+ DeferredAttribute):
+ defers.append(field.attname)
+ if pk_val is None:
+ # The pk_val and model values are the same for all
+ # DeferredAttribute classes, so we only need to do this
+ # once.
+ obj = self.__class__.__dict__[field.attname]
+ model = obj.model_ref()
+
return (model_unpickle, (model, defers), data)
def _get_pk_val(self, meta=None):
@@ -431,7 +437,7 @@ class Model(object):
else:
meta = cls._meta
- if origin:
+ if origin and not meta.auto_created:
signals.pre_save.send(sender=origin, instance=self, raw=raw)
# If we are in a raw save, save the object exactly as presented.
@@ -470,7 +476,7 @@ class Model(object):
if pk_set:
# Determine whether a record with the primary key already exists.
if (force_update or (not force_insert and
- manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by())):
+ manager.filter(pk=pk_val).exists())):
# It does already exist, so do an UPDATE.
if force_update or non_pks:
values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks]
@@ -504,7 +510,7 @@ class Model(object):
setattr(self, meta.pk.attname, result)
transaction.commit_unless_managed()
- if origin:
+ if origin and not meta.auto_created:
signals.post_save.send(sender=origin, instance=self,
created=(not record_exists), raw=raw)
@@ -541,7 +547,12 @@ class Model(object):
rel_descriptor = cls.__dict__[rel_opts_name]
break
else:
- raise AssertionError("Should never get here.")
+ # in the case of a hidden fkey just skip it, it'll get
+ # processed as an m2m
+ if not related.field.rel.is_hidden():
+ raise AssertionError("Should never get here.")
+ else:
+ continue
delete_qs = rel_descriptor.delete_manager(self).all()
for sub_obj in delete_qs:
sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index e0afcdb136..8c82ddb42c 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -58,6 +58,10 @@ def add_lazy_relation(cls, field, relation, operation):
# 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
+ app_label = relation._meta.app_label
+ model_name = relation._meta.object_name
# Try to look up the related model, and if it's already loaded resolve the
# string right away. If get_model returns None, it means that the related
@@ -96,7 +100,7 @@ class RelatedField(object):
self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()}
other = self.rel.to
- if isinstance(other, basestring):
+ if isinstance(other, basestring) or other._meta.pk is None:
def resolve_related_class(field, model, cls):
field.rel.to = model
field.do_related_class(model, cls)
@@ -401,22 +405,22 @@ class ForeignRelatedObjectsDescriptor(object):
return manager
-def create_many_related_manager(superclass, through=False):
+def create_many_related_manager(superclass, rel=False):
"""Creates a manager that subclasses 'superclass' (which is a Manager)
and adds behavior for many-to-many related objects."""
+ through = rel.through
class ManyRelatedManager(superclass):
def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None,
- join_table=None, source_col_name=None, target_col_name=None):
+ join_table=None, source_field_name=None, target_field_name=None):
super(ManyRelatedManager, self).__init__()
self.core_filters = core_filters
self.model = model
self.symmetrical = symmetrical
self.instance = instance
- self.join_table = join_table
- self.source_col_name = source_col_name
- self.target_col_name = target_col_name
+ self.source_field_name = source_field_name
+ self.target_field_name = target_field_name
self.through = through
- self._pk_val = self.instance._get_pk_val()
+ 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__)
@@ -425,36 +429,37 @@ def create_many_related_manager(superclass, through=False):
# If the ManyToMany relation has an intermediary model,
# the add and remove methods do not exist.
- if through is None:
+ if rel.through._meta.auto_created:
def add(self, *objs):
- self._add_items(self.source_col_name, self.target_col_name, *objs)
+ self._add_items(self.source_field_name, self.target_field_name, *objs)
# If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table
if self.symmetrical:
- self._add_items(self.target_col_name, self.source_col_name, *objs)
+ self._add_items(self.target_field_name, self.source_field_name, *objs)
add.alters_data = True
def remove(self, *objs):
- self._remove_items(self.source_col_name, self.target_col_name, *objs)
+ self._remove_items(self.source_field_name, self.target_field_name, *objs)
# If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
if self.symmetrical:
- self._remove_items(self.target_col_name, self.source_col_name, *objs)
+ self._remove_items(self.target_field_name, self.source_field_name, *objs)
remove.alters_data = True
def clear(self):
- self._clear_items(self.source_col_name)
+ self._clear_items(self.source_field_name)
# If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table
if self.symmetrical:
- self._clear_items(self.target_col_name)
+ self._clear_items(self.target_field_name)
clear.alters_data = True
def create(self, **kwargs):
# This check needs to be done here, since we can't later remove this
# from the method lookup table, as we do with add and remove.
- if through is not None:
- raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
+ if not rel.through._meta.auto_created:
+ opts = through._meta
+ raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
new_obj = super(ManyRelatedManager, self).create(**kwargs)
self.add(new_obj)
return new_obj
@@ -470,41 +475,38 @@ def create_many_related_manager(superclass, through=False):
return obj, created
get_or_create.alters_data = True
- def _add_items(self, source_col_name, target_col_name, *objs):
+ def _add_items(self, source_field_name, target_field_name, *objs):
# join_table: name of the m2m link table
- # source_col_name: the PK colname in join_table for the source object
- # target_col_name: the PK colname in join_table for the target object
+ # source_field_name: the PK fieldname in join_table for the source object
+ # target_col_name: the PK fieldname in join_table for the target object
# *objs - objects to add. Either object instances, or primary keys of object instances.
# If there aren't any objects, there is nothing to do.
+ from django.db.models import Model
if objs:
- from django.db.models.base import Model
- # Check that all the objects are of the right type
new_ids = set()
for obj in objs:
if isinstance(obj, self.model):
- new_ids.add(obj._get_pk_val())
+ new_ids.add(obj.pk)
elif isinstance(obj, Model):
raise TypeError, "'%s' instance expected" % self.model._meta.object_name
else:
new_ids.add(obj)
- # Add the newly created or already existing objects to the join table.
- # First find out which items are already added, to avoid adding them twice
- cursor = connection.cursor()
- cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \
- (target_col_name, self.join_table, source_col_name,
- target_col_name, ",".join(['%s'] * len(new_ids))),
- [self._pk_val] + list(new_ids))
- existing_ids = set([row[0] for row in cursor.fetchall()])
+ vals = self.through._default_manager.values_list(target_field_name, flat=True)
+ vals = vals.filter(**{
+ source_field_name: self._pk_val,
+ '%s__in' % target_field_name: new_ids,
+ })
+ vals = set(vals)
# Add the ones that aren't there already
- for obj_id in (new_ids - existing_ids):
- cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \
- (self.join_table, source_col_name, target_col_name),
- [self._pk_val, obj_id])
- transaction.commit_unless_managed()
+ for obj_id in (new_ids - vals):
+ self.through._default_manager.create(**{
+ '%s_id' % source_field_name: self._pk_val,
+ '%s_id' % target_field_name: obj_id,
+ })
- def _remove_items(self, source_col_name, target_col_name, *objs):
+ def _remove_items(self, source_field_name, target_field_name, *objs):
# source_col_name: the PK colname in join_table for the source object
# target_col_name: the PK colname in join_table for the target object
# *objs - objects to remove
@@ -515,24 +517,20 @@ def create_many_related_manager(superclass, through=False):
old_ids = set()
for obj in objs:
if isinstance(obj, self.model):
- old_ids.add(obj._get_pk_val())
+ old_ids.add(obj.pk)
else:
old_ids.add(obj)
# Remove the specified objects from the join table
- cursor = connection.cursor()
- cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \
- (self.join_table, source_col_name,
- target_col_name, ",".join(['%s'] * len(old_ids))),
- [self._pk_val] + list(old_ids))
- transaction.commit_unless_managed()
+ self.through._default_manager.filter(**{
+ source_field_name: self._pk_val,
+ '%s__in' % target_field_name: old_ids
+ }).delete()
- def _clear_items(self, source_col_name):
+ def _clear_items(self, source_field_name):
# source_col_name: the PK colname in join_table for the source object
- cursor = connection.cursor()
- cursor.execute("DELETE FROM %s WHERE %s = %%s" % \
- (self.join_table, source_col_name),
- [self._pk_val])
- transaction.commit_unless_managed()
+ self.through._default_manager.filter(**{
+ source_field_name: self._pk_val
+ }).delete()
return ManyRelatedManager
@@ -554,17 +552,15 @@ class ManyRelatedObjectsDescriptor(object):
# model's default manager.
rel_model = self.related.model
superclass = rel_model._default_manager.__class__
- RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through)
+ RelatedManager = create_many_related_manager(superclass, self.related.field.rel)
- qn = connection.ops.quote_name
manager = RelatedManager(
model=rel_model,
core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()},
instance=instance,
symmetrical=False,
- join_table=qn(self.related.field.m2m_db_table()),
- source_col_name=qn(self.related.field.m2m_reverse_name()),
- target_col_name=qn(self.related.field.m2m_column_name())
+ source_field_name=self.related.field.m2m_reverse_field_name(),
+ target_field_name=self.related.field.m2m_field_name()
)
return manager
@@ -573,9 +569,9 @@ class ManyRelatedObjectsDescriptor(object):
if instance is None:
raise AttributeError, "Manager must be accessed via instance"
- through = getattr(self.related.field.rel, 'through', None)
- if through is not None:
- raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
+ if not self.related.field.rel.through._meta.auto_created:
+ opts = self.related.field.rel.through._meta
+ raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
manager = self.__get__(instance)
manager.clear()
@@ -590,6 +586,9 @@ class ReverseManyRelatedObjectsDescriptor(object):
# ReverseManyRelatedObjectsDescriptor instance.
def __init__(self, m2m_field):
self.field = m2m_field
+ # through is provided so that you have easy access to the through
+ # model (Book.authors.through) for inlines, etc.
+ self.through = m2m_field.rel.through
def __get__(self, instance, instance_type=None):
if instance is None:
@@ -599,17 +598,15 @@ class ReverseManyRelatedObjectsDescriptor(object):
# model's default manager.
rel_model=self.field.rel.to
superclass = rel_model._default_manager.__class__
- RelatedManager = create_many_related_manager(superclass, self.field.rel.through)
+ RelatedManager = create_many_related_manager(superclass, self.field.rel)
- qn = connection.ops.quote_name
manager = RelatedManager(
model=rel_model,
core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()},
instance=instance,
symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)),
- join_table=qn(self.field.m2m_db_table()),
- source_col_name=qn(self.field.m2m_column_name()),
- target_col_name=qn(self.field.m2m_reverse_name())
+ source_field_name=self.field.m2m_field_name(),
+ target_field_name=self.field.m2m_reverse_field_name()
)
return manager
@@ -618,9 +615,9 @@ class ReverseManyRelatedObjectsDescriptor(object):
if instance is None:
raise AttributeError, "Manager must be accessed via instance"
- through = getattr(self.field.rel, 'through', None)
- if through is not None:
- raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through
+ if not self.field.rel.through._meta.auto_created:
+ opts = self.field.rel.through._meta
+ raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)
manager = self.__get__(instance)
manager.clear()
@@ -642,6 +639,10 @@ class ManyToOneRel(object):
self.multiple = True
self.parent_link = parent_link
+ def is_hidden(self):
+ "Should the related object be hidden?"
+ return self.related_name and self.related_name[-1] == '+'
+
def get_related_field(self):
"""
Returns the Field in the 'to' object to which this relationship is
@@ -673,6 +674,10 @@ class ManyToManyRel(object):
self.multiple = True
self.through = through
+ def is_hidden(self):
+ "Should the related object be hidden?"
+ return self.related_name and self.related_name[-1] == '+'
+
def get_related_field(self):
"""
Returns the field in the to' object to which this relationship is tied
@@ -693,7 +698,6 @@ class ForeignKey(RelatedField, Field):
assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT)
else:
assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name)
- to_field = to_field or to._meta.pk.name
kwargs['verbose_name'] = kwargs.get('verbose_name', None)
kwargs['rel'] = rel_class(to, to_field,
@@ -758,7 +762,12 @@ class ForeignKey(RelatedField, Field):
cls._meta.duplicate_targets[self.column] = (target, "o2m")
def contribute_to_related_class(self, cls, related):
- setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
+ # Internal FK's - i.e., those with a related name ending with '+' -
+ # don't get a related descriptor.
+ if not self.rel.is_hidden():
+ setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
+ if self.rel.field_name is None:
+ self.rel.field_name = cls._meta.pk.name
def formfield(self, **kwargs):
defaults = {
@@ -812,6 +821,51 @@ class OneToOneField(ForeignKey):
else:
setattr(instance, self.attname, data)
+def create_many_to_many_intermediary_model(field, klass):
+ from django.db import models
+ managed = True
+ if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT:
+ to = field.rel.to
+ to_model = field.rel.to
+ def set_managed(field, model, cls):
+ field.rel.through._meta.managed = model._meta.managed or cls._meta.managed
+ add_lazy_relation(klass, field, to_model, set_managed)
+ elif isinstance(field.rel.to, basestring):
+ to = klass._meta.object_name
+ to_model = klass
+ managed = klass._meta.managed
+ else:
+ to = field.rel.to._meta.object_name
+ to_model = field.rel.to
+ managed = klass._meta.managed or to_model._meta.managed
+ name = '%s_%s' % (klass._meta.object_name, field.name)
+ if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT or field.rel.to == klass._meta.object_name:
+ from_ = 'from_%s' % to.lower()
+ to = 'to_%s' % to.lower()
+ else:
+ from_ = klass._meta.object_name.lower()
+ to = to.lower()
+ meta = type('Meta', (object,), {
+ 'db_table': field._get_m2m_db_table(klass._meta),
+ 'managed': managed,
+ 'auto_created': klass,
+ 'unique_together': (from_, to)
+ })
+ # If the models have been split into subpackages, klass.__module__
+ # will be the subpackge, not the models module for the app. (See #12168)
+ # Compose the actual models module name by stripping the trailing parts
+ # of the namespace until we find .models
+ parts = klass.__module__.split('.')
+ while parts[-1] != 'models':
+ parts.pop()
+ module = '.'.join(parts)
+ # Construct and return the new class.
+ return type(name, (models.Model,), {
+ 'Meta': meta,
+ '__module__': module,
+ from_: models.ForeignKey(klass, related_name='%s+' % name),
+ to: models.ForeignKey(to_model, related_name='%s+' % name)
+ })
class ManyToManyField(RelatedField, Field):
def __init__(self, to, **kwargs):
@@ -829,10 +883,7 @@ class ManyToManyField(RelatedField, Field):
self.db_table = kwargs.pop('db_table', None)
if kwargs['rel'].through is not None:
- self.creates_table = False
assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used."
- else:
- self.creates_table = True
Field.__init__(self, **kwargs)
@@ -845,62 +896,45 @@ class ManyToManyField(RelatedField, Field):
def _get_m2m_db_table(self, opts):
"Function that can be curried to provide the m2m table name for this relation"
if self.rel.through is not None:
- return self.rel.through_model._meta.db_table
+ return self.rel.through._meta.db_table
elif self.db_table:
return self.db_table
else:
return util.truncate_name('%s_%s' % (opts.db_table, self.name),
connection.ops.max_name_length())
- def _get_m2m_column_name(self, related):
+ def _get_m2m_attr(self, related, attr):
"Function that can be curried to provide the source column name for the m2m table"
- try:
- return self._m2m_column_name_cache
- except:
- if self.rel.through is not None:
- for f in self.rel.through_model._meta.fields:
- if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
- self._m2m_column_name_cache = f.column
- break
- # If this is an m2m relation to self, avoid the inevitable name clash
- elif related.model == related.parent_model:
- self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id'
- else:
- self._m2m_column_name_cache = related.model._meta.object_name.lower() + '_id'
-
- # Return the newly cached value
- return self._m2m_column_name_cache
+ cache_attr = '_m2m_%s_cache' % attr
+ if hasattr(self, cache_attr):
+ return getattr(self, cache_attr)
+ for f in self.rel.through._meta.fields:
+ if hasattr(f,'rel') and f.rel and f.rel.to == related.model:
+ setattr(self, cache_attr, getattr(f, attr))
+ return getattr(self, cache_attr)
- def _get_m2m_reverse_name(self, related):
+ def _get_m2m_reverse_attr(self, related, attr):
"Function that can be curried to provide the related column name for the m2m table"
- try:
- return self._m2m_reverse_name_cache
- except:
- if self.rel.through is not None:
- found = False
- for f in self.rel.through_model._meta.fields:
- if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
- if related.model == related.parent_model:
- # If this is an m2m-intermediate to self,
- # the first foreign key you find will be
- # the source column. Keep searching for
- # the second foreign key.
- if found:
- self._m2m_reverse_name_cache = f.column
- break
- else:
- found = True
- else:
- self._m2m_reverse_name_cache = f.column
- break
- # If this is an m2m relation to self, avoid the inevitable name clash
- elif related.model == related.parent_model:
- self._m2m_reverse_name_cache = 'to_' + related.parent_model._meta.object_name.lower() + '_id'
- else:
- self._m2m_reverse_name_cache = related.parent_model._meta.object_name.lower() + '_id'
-
- # Return the newly cached value
- return self._m2m_reverse_name_cache
+ cache_attr = '_m2m_reverse_%s_cache' % attr
+ if hasattr(self, cache_attr):
+ return getattr(self, cache_attr)
+ found = False
+ for f in self.rel.through._meta.fields:
+ if hasattr(f,'rel') and f.rel and f.rel.to == related.parent_model:
+ if related.model == related.parent_model:
+ # If this is an m2m-intermediate to self,
+ # the first foreign key you find will be
+ # the source column. Keep searching for
+ # the second foreign key.
+ if found:
+ setattr(self, cache_attr, getattr(f, attr))
+ break
+ else:
+ found = True
+ else:
+ setattr(self, cache_attr, getattr(f, attr))
+ break
+ return getattr(self, cache_attr)
def isValidIDList(self, field_data, all_data):
"Validates that the value is a valid list of foreign keys"
@@ -942,10 +976,17 @@ class ManyToManyField(RelatedField, Field):
# specify *what* on my non-reversible relation?!"), so we set it up
# automatically. The funky name reduces the chance of an accidental
# clash.
- if self.rel.symmetrical and self.rel.to == "self" and self.rel.related_name is None:
+ if self.rel.symmetrical and (self.rel.to == "self" or self.rel.to == cls._meta.object_name):
self.rel.related_name = "%s_rel_+" % name
super(ManyToManyField, self).contribute_to_class(cls, name)
+
+ # The intermediate m2m model is not auto created if:
+ # 1) There is a manually specified intermediate, or
+ # 2) The class owning the m2m field is abstract.
+ if not self.rel.through and not cls._meta.abstract:
+ self.rel.through = create_many_to_many_intermediary_model(self, cls)
+
# Add the descriptor for the m2m relation
setattr(cls, self.name, ReverseManyRelatedObjectsDescriptor(self))
@@ -956,11 +997,8 @@ class ManyToManyField(RelatedField, Field):
# work correctly.
if isinstance(self.rel.through, basestring):
def resolve_through_model(field, model, cls):
- field.rel.through_model = model
+ field.rel.through = model
add_lazy_relation(cls, self, self.rel.through, resolve_through_model)
- elif self.rel.through:
- self.rel.through_model = self.rel.through
- self.rel.through = self.rel.through._meta.object_name
if isinstance(self.rel.to, basestring):
target = self.rel.to
@@ -969,15 +1007,17 @@ class ManyToManyField(RelatedField, Field):
cls._meta.duplicate_targets[self.column] = (target, "m2m")
def contribute_to_related_class(self, cls, related):
- # m2m relations to self do not have a ManyRelatedObjectsDescriptor,
- # as it would be redundant - unless the field is non-symmetrical.
- if related.model != related.parent_model or not self.rel.symmetrical:
- # Add the descriptor for the m2m relation
+ # Internal M2Ms (i.e., those with a related name ending with '+')
+ # don't get a related descriptor.
+ if not self.rel.is_hidden():
setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related))
# Set up the accessors for the column names on the m2m table
- self.m2m_column_name = curry(self._get_m2m_column_name, related)
- self.m2m_reverse_name = curry(self._get_m2m_reverse_name, related)
+ self.m2m_column_name = curry(self._get_m2m_attr, related, 'column')
+ self.m2m_reverse_name = curry(self._get_m2m_reverse_attr, related, 'column')
+
+ self.m2m_field_name = curry(self._get_m2m_attr, related, 'name')
+ self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name')
def set_attributes_from_rel(self):
pass
diff --git a/django/db/models/loading.py b/django/db/models/loading.py
index e07aab4efe..4ab1d5005a 100644
--- a/django/db/models/loading.py
+++ b/django/db/models/loading.py
@@ -131,19 +131,25 @@ class AppCache(object):
self._populate()
return self.app_errors
- def get_models(self, app_mod=None):
+ def get_models(self, app_mod=None, include_auto_created=False):
"""
Given a module containing models, returns a list of the models.
Otherwise returns a list of all installed models.
+
+ By default, auto-created models (i.e., m2m models without an
+ explicit intermediate table) are not included. However, if you
+ specify include_auto_created=True, they will be.
"""
self._populate()
if app_mod:
- return self.app_models.get(app_mod.__name__.split('.')[-2], SortedDict()).values()
+ model_list = self.app_models.get(app_mod.__name__.split('.')[-2], SortedDict()).values()
else:
model_list = []
for app_entry in self.app_models.itervalues():
model_list.extend(app_entry.values())
- return model_list
+ if not include_auto_created:
+ return filter(lambda o: not o._meta.auto_created, model_list)
+ return model_list
def get_model(self, app_label, model_name, seed_cache=True):
"""
diff --git a/django/db/models/manager.py b/django/db/models/manager.py
index 52612d8f64..7487fa0d46 100644
--- a/django/db/models/manager.py
+++ b/django/db/models/manager.py
@@ -1,5 +1,4 @@
import copy
-
from django.db.models.query import QuerySet, EmptyQuerySet, insert_query
from django.db.models import signals
from django.db.models.fields import FieldDoesNotExist
@@ -173,6 +172,9 @@ class Manager(object):
def only(self, *args, **kwargs):
return self.get_query_set().only(*args, **kwargs)
+ def exists(self, *args, **kwargs):
+ return self.get_query_set().exists(*args, **kwargs)
+
def _insert(self, values, **kwargs):
return insert_query(self.model, values, **kwargs)
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 34dd2aac34..05ff54a333 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -21,7 +21,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|
DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering',
'unique_together', 'permissions', 'get_latest_by',
'order_with_respect_to', 'app_label', 'db_tablespace',
- 'abstract', 'managed', 'proxy')
+ 'abstract', 'managed', 'proxy', 'auto_created')
class Options(object):
def __init__(self, meta, app_label=None):
@@ -47,6 +47,7 @@ class Options(object):
self.proxy_for_model = None
self.parents = SortedDict()
self.duplicate_targets = {}
+ self.auto_created = False
# To handle various inheritance situations, we need to track where
# managers came from (concrete or abstract base classes).
@@ -487,4 +488,3 @@ class Options(object):
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 46a86fc03c..36949ac390 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -2,20 +2,13 @@
The main QuerySet implementation. This provides the public API for the ORM.
"""
-try:
- set
-except NameError:
- from sets import Set as set # Python 2.3 fallback
-
from copy import deepcopy
-
from django.db import connection, transaction, IntegrityError
from django.db.models.aggregates import Aggregate
from django.db.models.fields import DateField
from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory
from django.db.models import signals, sql
-
# Used to control how many objects are worked with at once in some cases (e.g.
# when deleting objects).
CHUNK_SIZE = 100
@@ -444,6 +437,11 @@ class QuerySet(object):
return query.execute_sql(None)
_update.alters_data = True
+ def exists(self):
+ if self._result_cache is None:
+ return self.query.has_results()
+ return bool(self._result_cache)
+
##################################################
# PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS #
##################################################
@@ -1030,7 +1028,8 @@ def delete_objects(seen_objs):
# Pre-notify all instances to be deleted.
for pk_val, instance in items:
- signals.pre_delete.send(sender=cls, instance=instance)
+ if not cls._meta.auto_created:
+ signals.pre_delete.send(sender=cls, instance=instance)
pk_list = [pk for pk,instance in items]
del_query = sql.DeleteQuery(cls, connection)
@@ -1064,7 +1063,8 @@ def delete_objects(seen_objs):
if field.rel and field.null and field.rel.to in seen_objs:
setattr(instance, field.attname, None)
- signals.post_delete.send(sender=cls, instance=instance)
+ if not cls._meta.auto_created:
+ signals.post_delete.send(sender=cls, instance=instance)
setattr(instance, cls._meta.pk.attname, None)
if forced_managed:
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index 23f99e41ad..7bc45cbce2 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -8,7 +8,6 @@ all about the internals of models in order to get the information it needs.
"""
from copy import deepcopy
-
from django.utils.tree import Node
from django.utils.datastructures import SortedDict
from django.utils.encoding import force_unicode
@@ -24,11 +23,6 @@ from django.core.exceptions import FieldError
from datastructures import EmptyResultSet, Empty, MultiJoin
from constants import *
-try:
- set
-except NameError:
- from sets import Set as set # Python 2.3 fallback
-
__all__ = ['Query', 'BaseQuery']
class BaseQuery(object):
@@ -384,6 +378,16 @@ class BaseQuery(object):
return number
+ def has_results(self):
+ q = self.clone()
+ q.add_extra({'a': 1}, None, None, None, None, None)
+ q.add_fields(())
+ q.set_extra_mask(('a',))
+ q.set_aggregate_mask(())
+ q.clear_ordering()
+ q.set_limits(high=1)
+ return bool(q.execute_sql(SINGLE))
+
def as_sql(self, with_limits=True, with_col_aliases=False):
"""
Creates the SQL for this query. Returns the SQL string and list of
diff --git a/django/forms/models.py b/django/forms/models.py
index 6ed8176f5f..11eebb780a 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -259,6 +259,163 @@ class BaseModelForm(BaseForm):
return self.cleaned_data
+ def validate_unique(self):
+ unique_checks, date_checks = self._get_unique_checks()
+ form_errors = []
+ bad_fields = set()
+
+ field_errors, global_errors = self._perform_unique_checks(unique_checks)
+ bad_fields.union(field_errors)
+ form_errors.extend(global_errors)
+
+ field_errors, global_errors = self._perform_date_checks(date_checks)
+ bad_fields.union(field_errors)
+ form_errors.extend(global_errors)
+
+ for field_name in bad_fields:
+ del self.cleaned_data[field_name]
+ if form_errors:
+ # Raise the unique together errors since they are considered
+ # form-wide.
+ raise ValidationError(form_errors)
+
+ def _get_unique_checks(self):
+ from django.db.models.fields import FieldDoesNotExist, Field as ModelField
+
+ # Gather a list of checks to perform. We only perform unique checks
+ # for fields present and not None in cleaned_data. Since this is a
+ # ModelForm, some fields may have been excluded; we can't perform a unique
+ # check on a form that is missing fields involved in that check. It also does
+ # not make sense to check data that didn't validate, and since NULL does not
+ # equal NULL in SQL we should not do any unique checking for NULL values.
+ unique_checks = []
+ # these are checks for the unique_for_<date/year/month>
+ date_checks = []
+ for check in self.instance._meta.unique_together[:]:
+ fields_on_form = [field for field in check if self.cleaned_data.get(field) is not None]
+ if len(fields_on_form) == len(check):
+ unique_checks.append(check)
+
+ # Gather a list of checks for fields declared as unique and add them to
+ # the list of checks. Again, skip empty fields and any that did not validate.
+ for name in self.fields:
+ try:
+ f = self.instance._meta.get_field_by_name(name)[0]
+ except FieldDoesNotExist:
+ # This is an extra field that's not on the ModelForm, ignore it
+ continue
+ if not isinstance(f, ModelField):
+ # This is an extra field that happens to have a name that matches,
+ # for example, a related object accessor for this model. So
+ # get_field_by_name found it, but it is not a Field so do not proceed
+ # to use it as if it were.
+ continue
+ if self.cleaned_data.get(name) is None:
+ continue
+ if f.unique:
+ unique_checks.append((name,))
+ if f.unique_for_date and self.cleaned_data.get(f.unique_for_date) is not None:
+ date_checks.append(('date', name, f.unique_for_date))
+ if f.unique_for_year and self.cleaned_data.get(f.unique_for_year) is not None:
+ date_checks.append(('year', name, f.unique_for_year))
+ if f.unique_for_month and self.cleaned_data.get(f.unique_for_month) is not None:
+ date_checks.append(('month', name, f.unique_for_month))
+ return unique_checks, date_checks
+
+
+ def _perform_unique_checks(self, unique_checks):
+ bad_fields = set()
+ form_errors = []
+
+ for unique_check in unique_checks:
+ # Try to look up an existing object with the same values as this
+ # object's values for all the unique field.
+
+ lookup_kwargs = {}
+ for field_name in unique_check:
+ lookup_value = self.cleaned_data[field_name]
+ # ModelChoiceField will return an object instance rather than
+ # a raw primary key value, so convert it to a pk value before
+ # using it in a lookup.
+ if isinstance(self.fields[field_name], ModelChoiceField):
+ lookup_value = lookup_value.pk
+ lookup_kwargs[str(field_name)] = lookup_value
+
+ qs = self.instance.__class__._default_manager.filter(**lookup_kwargs)
+
+ # Exclude the current object from the query if we are editing an
+ # instance (as opposed to creating a new one)
+ if self.instance.pk is not None:
+ qs = qs.exclude(pk=self.instance.pk)
+
+ if qs.exists():
+ if len(unique_check) == 1:
+ self._errors[unique_check[0]] = ErrorList([self.unique_error_message(unique_check)])
+ else:
+ form_errors.append(self.unique_error_message(unique_check))
+
+ # Mark these fields as needing to be removed from cleaned data
+ # later.
+ for field_name in unique_check:
+ bad_fields.add(field_name)
+ return bad_fields, form_errors
+
+ def _perform_date_checks(self, date_checks):
+ bad_fields = set()
+ for lookup_type, field, unique_for in date_checks:
+ lookup_kwargs = {}
+ # there's a ticket to add a date lookup, we can remove this special
+ # case if that makes it's way in
+ if lookup_type == 'date':
+ date = self.cleaned_data[unique_for]
+ lookup_kwargs['%s__day' % unique_for] = date.day
+ lookup_kwargs['%s__month' % unique_for] = date.month
+ lookup_kwargs['%s__year' % unique_for] = date.year
+ else:
+ lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(self.cleaned_data[unique_for], lookup_type)
+ lookup_kwargs[field] = self.cleaned_data[field]
+
+ qs = self.instance.__class__._default_manager.filter(**lookup_kwargs)
+ # Exclude the current object from the query if we are editing an
+ # instance (as opposed to creating a new one)
+ if self.instance.pk is not None:
+ qs = qs.exclude(pk=self.instance.pk)
+
+ if qs.exists():
+ self._errors[field] = ErrorList([
+ self.date_error_message(lookup_type, field, unique_for)
+ ])
+ bad_fields.add(field)
+ return bad_fields, []
+
+ def date_error_message(self, lookup_type, field, unique_for):
+ return _(u"%(field_name)s must be unique for %(date_field)s %(lookup)s.") % {
+ 'field_name': unicode(self.fields[field].label),
+ 'date_field': unicode(self.fields[unique_for].label),
+ 'lookup': lookup_type,
+ }
+
+ def unique_error_message(self, unique_check):
+ model_name = capfirst(self.instance._meta.verbose_name)
+
+ # A unique field
+ if len(unique_check) == 1:
+ field_name = unique_check[0]
+ field_label = self.fields[field_name].label
+ # Insert the error into the error dict, very sneaky
+ return _(u"%(model_name)s with this %(field_label)s already exists.") % {
+ 'model_name': unicode(model_name),
+ 'field_label': unicode(field_label)
+ }
+ # unique_together
+ else:
+ field_labels = [self.fields[field_name].label for field_name in unique_check]
+ field_labels = get_text_list(field_labels, _('and'))
+ return _(u"%(model_name)s with this %(field_label)s already exists.") % {
+ 'model_name': unicode(model_name),
+ 'field_label': unicode(field_labels)
+ }
+
def save(self, commit=True):
"""
Saves this ``form``'s cleaned_data into model instance
@@ -577,7 +734,7 @@ class BaseInlineFormSet(BaseModelFormSet):
save_as_new=False, prefix=None):
from django.db.models.fields.related import RelatedObject
if instance is None:
- self.instance = self.model()
+ self.instance = self.fk.rel.to()
else:
self.instance = instance
self.save_as_new = save_as_new
diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py
new file mode 100644
index 0000000000..9ca727fca9
--- /dev/null
+++ b/django/middleware/csrf.py
@@ -0,0 +1,265 @@
+"""
+Cross Site Request Forgery Middleware.
+
+This module provides a middleware that implements protection
+against request forgeries from other sites.
+"""
+
+import itertools
+import re
+import random
+
+from django.conf import settings
+from django.core.urlresolvers import get_callable
+from django.utils.cache import patch_vary_headers
+from django.utils.hashcompat import md5_constructor
+from django.utils.safestring import mark_safe
+
+_POST_FORM_RE = \
+ re.compile(r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE)
+
+_HTML_TYPES = ('text/html', 'application/xhtml+xml')
+
+# Use the system (hardware-based) random number generator if it exists.
+if hasattr(random, 'SystemRandom'):
+ randrange = random.SystemRandom().randrange
+else:
+ randrange = random.randrange
+_MAX_CSRF_KEY = 18446744073709551616L # 2 << 63
+
+def _get_failure_view():
+ """
+ Returns the view to be used for CSRF rejections
+ """
+ return get_callable(settings.CSRF_FAILURE_VIEW)
+
+def _get_new_csrf_key():
+ return md5_constructor("%s%s"
+ % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest()
+
+def _make_legacy_session_token(session_id):
+ return md5_constructor(settings.SECRET_KEY + session_id).hexdigest()
+
+def get_token(request):
+ """
+ Returns the the CSRF token required for a POST form.
+
+ A side effect of calling this function is to make the the csrf_protect
+ decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
+ header to the outgoing response. For this reason, you may need to use this
+ function lazily, as is done by the csrf context processor.
+ """
+ request.META["CSRF_COOKIE_USED"] = True
+ return request.META.get("CSRF_COOKIE", None)
+
+class CsrfViewMiddleware(object):
+ """
+ Middleware that requires a present and correct csrfmiddlewaretoken
+ for POST requests that have a CSRF cookie, and sets an outgoing
+ CSRF cookie.
+
+ This middleware should be used in conjunction with the csrf_token template
+ tag.
+ """
+ def process_view(self, request, callback, callback_args, callback_kwargs):
+ if getattr(callback, 'csrf_exempt', False):
+ return None
+
+ if getattr(request, 'csrf_processing_done', False):
+ return None
+
+ reject = lambda s: _get_failure_view()(request, reason=s)
+ def accept():
+ # Avoid checking the request twice by adding a custom attribute to
+ # request. This will be relevant when both decorator and middleware
+ # are used.
+ request.csrf_processing_done = True
+ return None
+
+ # If the user doesn't have a CSRF cookie, generate one and store it in the
+ # request, so it's available to the view. We'll store it in a cookie when
+ # we reach the response.
+ try:
+ request.META["CSRF_COOKIE"] = request.COOKIES[settings.CSRF_COOKIE_NAME]
+ cookie_is_new = False
+ except KeyError:
+ # No cookie, so create one. This will be sent with the next
+ # response.
+ request.META["CSRF_COOKIE"] = _get_new_csrf_key()
+ # Set a flag to allow us to fall back and allow the session id in
+ # place of a CSRF cookie for this request only.
+ cookie_is_new = True
+
+ if request.method == 'POST':
+ if getattr(request, '_dont_enforce_csrf_checks', False):
+ # Mechanism to turn off CSRF checks for test suite. It comes after
+ # the creation of CSRF cookies, so that everything else continues to
+ # work exactly the same (e.g. cookies are sent etc), but before the
+ # any branches that call reject()
+ return accept()
+
+ if request.is_ajax():
+ # .is_ajax() is based on the presence of X-Requested-With. In
+ # the context of a browser, this can only be sent if using
+ # XmlHttpRequest. Browsers implement careful policies for
+ # XmlHttpRequest:
+ #
+ # * Normally, only same-domain requests are allowed.
+ #
+ # * Some browsers (e.g. Firefox 3.5 and later) relax this
+ # carefully:
+ #
+ # * if it is a 'simple' GET or POST request (which can
+ # include no custom headers), it is allowed to be cross
+ # domain. These requests will not be recognized as AJAX.
+ #
+ # * if a 'preflight' check with the server confirms that the
+ # server is expecting and allows the request, cross domain
+ # requests even with custom headers are allowed. These
+ # requests will be recognized as AJAX, but can only get
+ # through when the developer has specifically opted in to
+ # allowing the cross-domain POST request.
+ #
+ # So in all cases, it is safe to allow these requests through.
+ return accept()
+
+ if request.is_secure():
+ # Strict referer checking for HTTPS
+ referer = request.META.get('HTTP_REFERER')
+ if referer is None:
+ return reject("Referer checking failed - no Referer.")
+
+ # The following check ensures that the referer is HTTPS,
+ # the domains match and the ports match. This might be too strict.
+ good_referer = 'https://%s/' % request.get_host()
+ if not referer.startswith(good_referer):
+ return reject("Referer checking failed - %s does not match %s." %
+ (referer, good_referer))
+
+ # If the user didn't already have a CSRF cookie, then fall back to
+ # the Django 1.1 method (hash of session ID), so a request is not
+ # rejected if the form was sent to the user before upgrading to the
+ # Django 1.2 method (session independent nonce)
+ if cookie_is_new:
+ try:
+ session_id = request.COOKIES[settings.SESSION_COOKIE_NAME]
+ csrf_token = _make_legacy_session_token(session_id)
+ except KeyError:
+ # No CSRF cookie and no session cookie. For POST requests,
+ # we insist on a CSRF cookie, and in this way we can avoid
+ # all CSRF attacks, including login CSRF.
+ return reject("No CSRF or session cookie.")
+ else:
+ csrf_token = request.META["CSRF_COOKIE"]
+
+ # check incoming token
+ request_csrf_token = request.POST.get('csrfmiddlewaretoken', None)
+ if request_csrf_token != csrf_token:
+ if cookie_is_new:
+ # probably a problem setting the CSRF cookie
+ return reject("CSRF cookie not set.")
+ else:
+ return reject("CSRF token missing or incorrect.")
+
+ return accept()
+
+ def process_response(self, request, response):
+ if getattr(response, 'csrf_processing_done', False):
+ return response
+
+ # If CSRF_COOKIE is unset, then CsrfViewMiddleware.process_view was
+ # never called, probaby because a request middleware returned a response
+ # (for example, contrib.auth redirecting to a login page).
+ if request.META.get("CSRF_COOKIE") is None:
+ return response
+
+ if not request.META.get("CSRF_COOKIE_USED", False):
+ return response
+
+ # Set the CSRF cookie even if it's already set, so we renew the expiry timer.
+ response.set_cookie(settings.CSRF_COOKIE_NAME,
+ request.META["CSRF_COOKIE"], max_age = 60 * 60 * 24 * 7 * 52,
+ domain=settings.CSRF_COOKIE_DOMAIN)
+ # Content varies with the CSRF cookie, so set the Vary header.
+ patch_vary_headers(response, ('Cookie',))
+ response.csrf_processing_done = True
+ return response
+
+class CsrfResponseMiddleware(object):
+ """
+ DEPRECATED
+ Middleware that post-processes a response to add a csrfmiddlewaretoken.
+
+ This exists for backwards compatibility and as an interim measure until
+ applications are converted to using use the csrf_token template tag
+ instead. It will be removed in Django 1.4.
+ """
+ def __init__(self):
+ import warnings
+ warnings.warn(
+ "CsrfResponseMiddleware and CsrfMiddleware are deprecated; use CsrfViewMiddleware and the template tag instead (see CSRF documentation).",
+ PendingDeprecationWarning
+ )
+
+ def process_response(self, request, response):
+ if getattr(response, 'csrf_exempt', False):
+ return response
+
+ if response['Content-Type'].split(';')[0] in _HTML_TYPES:
+ csrf_token = get_token(request)
+ # If csrf_token is None, we have no token for this request, which probably
+ # means that this is a response from a request middleware.
+ if csrf_token is None:
+ return response
+
+ # ensure we don't add the 'id' attribute twice (HTML validity)
+ idattributes = itertools.chain(("id='csrfmiddlewaretoken'",),
+ itertools.repeat(''))
+ def add_csrf_field(match):
+ """Returns the matched <form> tag plus the added <input> element"""
+ return mark_safe(match.group() + "<div style='display:none;'>" + \
+ "<input type='hidden' " + idattributes.next() + \
+ " name='csrfmiddlewaretoken' value='" + csrf_token + \
+ "' /></div>")
+
+ # Modify any POST forms
+ response.content, n = _POST_FORM_RE.subn(add_csrf_field, response.content)
+ if n > 0:
+ # Content varies with the CSRF cookie, so set the Vary header.
+ patch_vary_headers(response, ('Cookie',))
+
+ # Since the content has been modified, any Etag will now be
+ # incorrect. We could recalculate, but only if we assume that
+ # the Etag was set by CommonMiddleware. The safest thing is just
+ # to delete. See bug #9163
+ del response['ETag']
+ return response
+
+class CsrfMiddleware(object):
+ """
+ Django middleware that adds protection against Cross Site
+ Request Forgeries by adding hidden form fields to POST forms and
+ checking requests for the correct value.
+
+ CsrfMiddleware uses two middleware, CsrfViewMiddleware and
+ CsrfResponseMiddleware, which can be used independently. It is recommended
+ to use only CsrfViewMiddleware and use the csrf_token template tag in
+ templates for inserting the token.
+ """
+ # We can't just inherit from CsrfViewMiddleware and CsrfResponseMiddleware
+ # because both have process_response methods.
+ def __init__(self):
+ self.response_middleware = CsrfResponseMiddleware()
+ self.view_middleware = CsrfViewMiddleware()
+
+ def process_response(self, request, resp):
+ # We must do the response post-processing first, because that calls
+ # get_token(), which triggers a flag saying that the CSRF cookie needs
+ # to be sent (done in CsrfViewMiddleware.process_response)
+ resp2 = self.response_middleware.process_response(request, resp)
+ return self.view_middleware.process_response(request, resp2)
+
+ def process_view(self, request, callback, callback_args, callback_kwargs):
+ return self.view_middleware.process_view(request, callback, callback_args,
+ callback_kwargs)
+
diff --git a/django/template/__init__.py b/django/template/__init__.py
index 5493e5bbb7..5b52d36089 100644
--- a/django/template/__init__.py
+++ b/django/template/__init__.py
@@ -942,8 +942,14 @@ class Library(object):
else:
t = get_template(file_name)
self.nodelist = t.nodelist
- return self.nodelist.render(context_class(dict,
- autoescape=context.autoescape))
+ new_context = context_class(dict, autoescape=context.autoescape)
+ # Copy across the CSRF token, if present, because inclusion
+ # tags are often used for forms, and we need instructions
+ # for using CSRF protection to be as simple as possible.
+ csrf_token = context.get('csrf_token', None)
+ if csrf_token is not None:
+ new_context['csrf_token'] = csrf_token
+ return self.nodelist.render(new_context)
compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode)
compile_func.__doc__ = func.__doc__
diff --git a/django/template/context.py b/django/template/context.py
index 1c43387468..f57a3aaa64 100644
--- a/django/template/context.py
+++ b/django/template/context.py
@@ -1,7 +1,12 @@
from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
+# Cache of actual callables.
_standard_context_processors = None
+# We need the CSRF processor no matter what the user has in their settings,
+# because otherwise it is a security vulnerability, and we can't afford to leave
+# this to human error or failure to read migration instructions.
+_builtin_context_processors = ('django.core.context_processors.csrf',)
class ContextPopException(Exception):
"pop() has been called more times than push()"
@@ -75,7 +80,10 @@ def get_standard_processors():
global _standard_context_processors
if _standard_context_processors is None:
processors = []
- for path in settings.TEMPLATE_CONTEXT_PROCESSORS:
+ collect = []
+ collect.extend(_builtin_context_processors)
+ collect.extend(settings.TEMPLATE_CONTEXT_PROCESSORS)
+ for path in collect:
i = path.rfind('.')
module, attr = path[:i], path[i+1:]
try:
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index 47e116cd1a..2957c3d045 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -162,7 +162,7 @@ def floatformat(text, arg=-1):
try:
m = int(d) - d
- except (OverflowError, InvalidOperation):
+ except (ValueError, OverflowError, InvalidOperation):
return input_val
if not m and p < 0:
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index de746997ab..6d57cdeef8 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -37,6 +37,23 @@ class CommentNode(Node):
def render(self, context):
return ''
+class CsrfTokenNode(Node):
+ def render(self, context):
+ csrf_token = context.get('csrf_token', None)
+ if csrf_token:
+ if csrf_token == 'NOTPROVIDED':
+ return mark_safe(u"")
+ else:
+ return mark_safe(u"<div style='display:none'><input type='hidden' name='csrfmiddlewaretoken' value='%s' /></div>" % (csrf_token))
+ else:
+ # It's very probable that the token is missing because of
+ # misconfiguration, so we raise a warning
+ from django.conf import settings
+ if settings.DEBUG:
+ import warnings
+ warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.")
+ return u''
+
class CycleNode(Node):
def __init__(self, cyclevars, variable_name=None):
self.cycle_iter = itertools_cycle(cyclevars)
@@ -523,6 +540,10 @@ def cycle(parser, token):
return node
cycle = register.tag(cycle)
+def csrf_token(parser, token):
+ return CsrfTokenNode()
+register.tag(csrf_token)
+
def debug(parser, token):
"""
Outputs a whole load of debugging information, including the current
diff --git a/django/test/client.py b/django/test/client.py
index 348bfa7d26..63ad1c1d3a 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -66,6 +66,11 @@ class ClientHandler(BaseHandler):
signals.request_started.send(sender=self.__class__)
try:
request = WSGIRequest(environ)
+ # sneaky little hack so that we can easily get round
+ # CsrfViewMiddleware. This makes life easier, and is probably
+ # required for backwards compatibility with external tests against
+ # admin views.
+ request._dont_enforce_csrf_checks = True
response = self.get_response(request)
# Apply response middleware.
@@ -362,12 +367,18 @@ class Client(object):
else:
post_data = data
+ # Make `data` into a querystring only if it's not already a string. If
+ # it is a string, we'll assume that the caller has already encoded it.
+ query_string = None
+ if not isinstance(data, basestring):
+ query_string = urlencode(data, doseq=True)
+
parsed = urlparse(path)
r = {
'CONTENT_LENGTH': len(post_data),
'CONTENT_TYPE': content_type,
'PATH_INFO': urllib.unquote(parsed[2]),
- 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
+ 'QUERY_STRING': query_string or parsed[4],
'REQUEST_METHOD': 'PUT',
'wsgi.input': FakePayload(post_data),
}
diff --git a/django/test/utils.py b/django/test/utils.py
index d34dd33d15..9d39eee926 100644
--- a/django/test/utils.py
+++ b/django/test/utils.py
@@ -2,6 +2,7 @@ import sys, time, os
from django.conf import settings
from django.db import connection
from django.core import mail
+from django.core.mail.backends import locmem
from django.test import signals
from django.template import Template
from django.utils.translation import deactivate
@@ -28,37 +29,22 @@ def instrumented_test_render(self, context):
signals.template_rendered.send(sender=self, template=self, context=context)
return self.nodelist.render(context)
-class TestSMTPConnection(object):
- """A substitute SMTP connection for use during test sessions.
- The test connection stores email messages in a dummy outbox,
- rather than sending them out on the wire.
-
- """
- def __init__(*args, **kwargs):
- pass
- def open(self):
- "Mock the SMTPConnection open() interface"
- pass
- def close(self):
- "Mock the SMTPConnection close() interface"
- pass
- def send_messages(self, messages):
- "Redirect messages to the dummy outbox"
- mail.outbox.extend(messages)
- return len(messages)
def setup_test_environment():
"""Perform any global pre-test setup. This involves:
- Installing the instrumented test renderer
- - Diverting the email sending functions to a test buffer
+ - Set the email backend to the locmem email backend.
- Setting the active locale to match the LANGUAGE_CODE setting.
"""
Template.original_render = Template.render
Template.render = instrumented_test_render
mail.original_SMTPConnection = mail.SMTPConnection
- mail.SMTPConnection = TestSMTPConnection
+ mail.SMTPConnection = locmem.EmailBackend
+
+ mail.original_email_backend = settings.EMAIL_BACKEND
+ settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem'
mail.outbox = []
@@ -77,8 +63,10 @@ def teardown_test_environment():
mail.SMTPConnection = mail.original_SMTPConnection
del mail.original_SMTPConnection
- del mail.outbox
+ settings.EMAIL_BACKEND = mail.original_email_backend
+ del mail.original_email_backend
+ del mail.outbox
def get_runner(settings):
test_path = settings.TEST_RUNNER.split('.')
diff --git a/django/utils/decorators.py b/django/utils/decorators.py
index 4636a2d040..e3f16e462e 100644
--- a/django/utils/decorators.py
+++ b/django/utils/decorators.py
@@ -6,6 +6,19 @@ try:
except ImportError:
from django.utils.functional import wraps, update_wrapper # Python 2.3, 2.4 fallback.
+
+# Licence for MethodDecoratorAdaptor and auto_adapt_to_methods
+#
+# This code is taken from stackoverflow.com [1], the code being supplied by
+# users 'Ants Aasma' [2] and 'Silent Ghost' [3] with modifications. It is
+# legally included here under the terms of the Creative Commons
+# Attribution-Share Alike 2.5 Generic Licence [4]
+#
+# [1] http://stackoverflow.com/questions/1288498/using-the-same-decorator-with-arguments-with-functions-and-methods
+# [2] http://stackoverflow.com/users/107366/ants-aasma
+# [3] http://stackoverflow.com/users/12855/silentghost
+# [4] http://creativecommons.org/licenses/by-sa/2.5/
+
class MethodDecoratorAdaptor(object):
"""
Generic way of creating decorators that adapt to being
diff --git a/django/utils/functional.py b/django/utils/functional.py
index 23614d1712..434b6b76c9 100644
--- a/django/utils/functional.py
+++ b/django/utils/functional.py
@@ -257,9 +257,8 @@ class LazyObject(object):
A wrapper for another class that can be used to delay instantiation of the
wrapped class.
- This is useful, for example, if the wrapped class needs to use Django
- settings at creation time: we want to permit it to be imported without
- accessing settings.
+ By subclassing, you have the opportunity to intercept and alter the
+ instantiation. If you don't need to do that, use SimpleLazyObject.
"""
def __init__(self):
self._wrapped = None
@@ -267,9 +266,6 @@ class LazyObject(object):
def __getattr__(self, name):
if self._wrapped is None:
self._setup()
- if name == "__members__":
- # Used to implement dir(obj)
- return self._wrapped.get_all_members()
return getattr(self._wrapped, name)
def __setattr__(self, name, value):
@@ -287,3 +283,68 @@ class LazyObject(object):
"""
raise NotImplementedError
+ # introspection support:
+ __members__ = property(lambda self: self.__dir__())
+
+ def __dir__(self):
+ if self._wrapped is None:
+ self._setup()
+ return dir(self._wrapped)
+
+class SimpleLazyObject(LazyObject):
+ """
+ A lazy object initialised from any function.
+
+ Designed for compound objects of unknown type. For builtins or objects of
+ known type, use django.utils.functional.lazy.
+ """
+ def __init__(self, func):
+ """
+ Pass in a callable that returns the object to be wrapped.
+
+ If copies are made of the resulting SimpleLazyObject, which can happen
+ in various circumstances within Django, then you must ensure that the
+ callable can be safely run more than once and will return the same
+ value.
+ """
+ self.__dict__['_setupfunc'] = func
+ # For some reason, we have to inline LazyObject.__init__ here to avoid
+ # recursion
+ self._wrapped = None
+
+ def __str__(self):
+ if self._wrapped is None: self._setup()
+ return str(self._wrapped)
+
+ def __unicode__(self):
+ if self._wrapped is None: self._setup()
+ return unicode(self._wrapped)
+
+ def __deepcopy__(self, memo):
+ if self._wrapped is None:
+ # We have to use SimpleLazyObject, not self.__class__, because the
+ # latter is proxied.
+ result = SimpleLazyObject(self._setupfunc)
+ memo[id(self)] = result
+ return result
+ else:
+ import copy
+ return copy.deepcopy(self._wrapped, memo)
+
+ # Need to pretend to be the wrapped class, for the sake of objects that care
+ # about this (especially in equality tests)
+ def __get_class(self):
+ if self._wrapped is None: self._setup()
+ return self._wrapped.__class__
+ __class__ = property(__get_class)
+
+ def __eq__(self, other):
+ if self._wrapped is None: self._setup()
+ return self._wrapped == other
+
+ def __hash__(self):
+ if self._wrapped is None: self._setup()
+ return hash(self._wrapped)
+
+ def _setup(self):
+ self._wrapped = self._setupfunc()
diff --git a/django/views/csrf.py b/django/views/csrf.py
new file mode 100644
index 0000000000..aa5e25b5b4
--- /dev/null
+++ b/django/views/csrf.py
@@ -0,0 +1,69 @@
+from django.http import HttpResponseForbidden
+from django.template import Context, Template
+from django.conf import settings
+
+# We include the template inline since we need to be able to reliably display
+# this error message, especially for the sake of developers, and there isn't any
+# other way of making it available independent of what is in the settings file.
+
+CSRF_FAILRE_TEMPLATE = """
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html lang="en">
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <title>403 Forbidden</title>
+</head>
+<body>
+ <h1>403 Forbidden</h1>
+ <p>CSRF verification failed. Request aborted.</p>
+ {% if DEBUG %}
+ <h2>Help</h2>
+ {% if reason %}
+ <p>Reason given for failure:</p>
+ <pre>
+ {{ reason }}
+ </pre>
+ {% endif %}
+
+ <p>In general, this can occur when there is a genuine Cross Site Request Forgery, or when
+ <a
+ href='http://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ref-contrib-csrf'>Django's
+ CSRF mechanism</a> has not been used correctly. For POST forms, you need to
+ ensure:</p>
+
+ <ul>
+ <li>The view function uses <a
+ href='http://docs.djangoproject.com/en/dev/ref/templates/api/#subclassing-context-requestcontext'><code>RequestContext</code></a>
+ for the template, instead of <code>Context</code>.</li>
+
+ <li>In the template, there is a <code>{% templatetag openblock %} csrf_token
+ {% templatetag closeblock %}</code> template tag inside each POST form that
+ targets an internal URL.</li>
+
+ <li>If you are not using <code>CsrfViewMiddleware</code>, then you must use
+ <code>csrf_protect</code> on any views that use the <code>csrf_token</code>
+ template tag, as well as those that accept the POST data.</li>
+
+ </ul>
+
+ <p>You're seeing the help section of this page because you have <code>DEBUG =
+ True</code> in your Django settings file. Change that to <code>False</code>,
+ and only the initial error message will be displayed. </p>
+
+ <p>You can customize this page using the CSRF_FAILURE_VIEW setting.</p>
+ {% else %}
+ <p><small>More information is available with DEBUG=True.</small></p>
+
+ {% endif %}
+</body>
+</html>
+"""
+
+def csrf_failure(request, reason=""):
+ """
+ Default view used when request fails CSRF protection
+ """
+ t = Template(CSRF_FAILRE_TEMPLATE)
+ c = Context({'DEBUG': settings.DEBUG,
+ 'reason': reason})
+ return HttpResponseForbidden(t.render(c), mimetype='text/html')
diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py
new file mode 100644
index 0000000000..b789872efe
--- /dev/null
+++ b/django/views/decorators/csrf.py
@@ -0,0 +1,47 @@
+from django.middleware.csrf import CsrfViewMiddleware
+from django.utils.decorators import decorator_from_middleware
+try:
+ from functools import wraps
+except ImportError:
+ from django.utils.functional import wraps # Python 2.3, 2.4 fallback.
+
+csrf_protect = decorator_from_middleware(CsrfViewMiddleware)
+csrf_protect.__name__ = "csrf_protect"
+csrf_protect.__doc__ = """
+This decorator adds CSRF protection in exactly the same way as
+CsrfViewMiddleware, but it can be used on a per view basis. Using both, or
+using the decorator multiple times, is harmless and efficient.
+"""
+
+def csrf_response_exempt(view_func):
+ """
+ Modifies a view function so that its response is exempt
+ from the post-processing of the CSRF middleware.
+ """
+ def wrapped_view(*args, **kwargs):
+ resp = view_func(*args, **kwargs)
+ resp.csrf_exempt = True
+ return resp
+ return wraps(view_func)(wrapped_view)
+
+def csrf_view_exempt(view_func):
+ """
+ Marks a view function as being exempt from CSRF view protection.
+ """
+ # We could just do view_func.csrf_exempt = True, but decorators
+ # are nicer if they don't have side-effects, so we return a new
+ # function.
+ def wrapped_view(*args, **kwargs):
+ return view_func(*args, **kwargs)
+ wrapped_view.csrf_exempt = True
+ return wraps(view_func)(wrapped_view)
+
+def csrf_exempt(view_func):
+ """
+ Marks a view function as being exempt from the CSRF checks
+ and post processing.
+
+ This is the same as using both the csrf_view_exempt and
+ csrf_response_exempt decorators.
+ """
+ return csrf_response_exempt(csrf_view_exempt(view_func))
diff --git a/docs/faq/install.txt b/docs/faq/install.txt
index fb8005c7e7..28a89ccc1f 100644
--- a/docs/faq/install.txt
+++ b/docs/faq/install.txt
@@ -18,7 +18,7 @@ How do I get started?
What are Django's prerequisites?
--------------------------------
-Django requires Python_, specifically any version of Python from 2.3
+Django requires Python_, specifically any version of Python from 2.4
through 2.6. No other Python libraries are required for basic Django
usage.
@@ -42,30 +42,35 @@ PostgreSQL fans, and MySQL_, `SQLite 3`_, and Oracle_ are also supported.
.. _`SQLite 3`: http://www.sqlite.org/
.. _Oracle: http://www.oracle.com/
-Do I lose anything by using Python 2.3 versus newer Python versions, such as Python 2.5?
-----------------------------------------------------------------------------------------
+Do I lose anything by using Python 2.4 versus newer Python versions, such as Python 2.5 or 2.6?
+-----------------------------------------------------------------------------------------------
-Not in the core framework. Currently, Django itself officially
-supports any version of Python from 2.3 through 2.6,
-inclusive. However, some add-on components may require a more recent
-Python version; the ``django.contrib.gis`` component, for example,
-requires at least Python 2.4, and third-party applications for use
-with Django are, of course, free to set their own version
-requirements.
+Not in the core framework. Currently, Django itself officially supports any
+version of Python from 2.4 through 2.6, inclusive. However, newer versions of
+Python are often faster, have more features, and are better supported.
+Third-party applications for use with Django are, of course, free to set their
+own version requirements.
-Please note, however, that over the next year or two Django will begin
-dropping support for older Python versions as part of a migration
-which will end with Django running on Python 3.0 (see next question
-for details). So if you're just starting out with Python, it's
-recommended that you use the latest 2.x release (currently, Python
-2.6). This will let you take advantage of the numerous improvements
-and optimizations to the Python language since version 2.3, and will
-help ease the process of dropping support for older Python versions on
-the road to Python 3.0.
+Over the next year or two Django will begin dropping support for older Python
+versions as part of a migration which will end with Django running on Python 3
+(see below for details).
-Can I use Django with Python 3.0?
+All else being equal, we recommend that you use the latest 2.x release
+(currently Python 2.6). This will let you take advantage of the numerous
+improvements and optimizations to the Python language since version 2.4, and
+will help ease the process of dropping support for older Python versions on
+the road to Python 3.
+
+Can I use Django with Python 2.3?
---------------------------------
+Django 1.1 (and earlier) supported Python 2.3. Django 1.2 and newer does not.
+We highly recommend you upgrade Python if at all possible, but Django 1.1 will
+continue to work on Python 2.3.
+
+Can I use Django with Python 3?
+-------------------------------
+
Not at the moment. Python 3.0 introduced a number of
backwards-incompatible changes to the Python language, and although
these changes are generally a good thing for Python's future, it will
diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt
index 7326532ec9..803c3140c7 100644
--- a/docs/internals/committers.txt
+++ b/docs/internals/committers.txt
@@ -148,7 +148,6 @@ Joseph Kocherhans
.. _brian rosner: http://oebfare.com/
.. _eldarion: http://eldarion.com/
-.. _pinax: http://pinaxproject.com/
.. _django dose: http://djangodose.com/
`Gary Wilson`_
@@ -189,6 +188,18 @@ Karen Tracey
Karen lives in Apex, NC, USA.
+`Jannis Leidel`_
+ Jannis graduated in media design from `Bauhaus-University Weimar`_,
+ is the author of a number of pluggable Django apps and likes to
+ contribute to Open Source projects like Pinax_. He currently works as
+ a freelance web developer and designer.
+
+ Jannis lives in Berlin, Germany.
+
+.. _Jannis Leidel: http://jezdez.com/
+.. _Bauhaus-University Weimar: http://www.uni-weimar.de/
+.. _pinax: http://pinaxproject.com/
+
Specialists
-----------
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 7e7f4c6338..480b527d6b 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -13,6 +13,21 @@ their deprecation, as per the :ref:`Django deprecation policy
hooking up admin URLs. This has been deprecated since the 1.1
release.
+ * 1.4
+ * ``CsrfResponseMiddleware``. This has been deprecated since the 1.2
+ release, in favour of the template tag method for inserting the CSRF
+ token. ``CsrfMiddleware``, which combines ``CsrfResponseMiddleware``
+ and ``CsrfViewMiddleware``, is also deprecated.
+
+ * The old imports for CSRF functionality (``django.contrib.csrf.*``),
+ which moved to core in 1.2, will be removed.
+
+ * ``SMTPConnection``. The 1.2 release deprecated the ``SMTPConnection``
+ class in favor of a generic E-mail backend API.
+
+ * The many to many SQL generation functions on the database backends
+ will be removed. These have been deprecated since the 1.2 release.
+
* 2.0
* ``django.views.defaults.shortcut()``. This function has been moved
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
diff --git a/docs/internals/release-process.txt b/docs/internals/release-process.txt
index 6d4ad9e8c9..e990ab8ab6 100644
--- a/docs/internals/release-process.txt
+++ b/docs/internals/release-process.txt
@@ -56,7 +56,7 @@ These releases will contain new features, improvements to existing features, and
such. A minor release may deprecate certain features from previous releases. If a
feature in version ``A.B`` is deprecated, it will continue to work in version
``A.B+1``. In version ``A.B+2``, use of the feature will raise a
-``PendingDeprecationWarning`` but will continue to work. Version ``A.B+3`` will
+``DeprecationWarning`` but will continue to work. Version ``A.B+3`` will
remove the feature entirely.
So, for example, if we decided to remove a function that existed in Django 1.0:
diff --git a/docs/intro/install.txt b/docs/intro/install.txt
index 237c208f2a..d0776a6ea3 100644
--- a/docs/intro/install.txt
+++ b/docs/intro/install.txt
@@ -12,7 +12,7 @@ Install Python
--------------
Being a Python Web framework, Django requires Python. It works with any Python
-version from 2.3 to 2.6 (due to backwards
+version from 2.4 to 2.6 (due to backwards
incompatibilities in Python 3.0, Django does not currently work with
Python 3.0; see :ref:`the Django FAQ <faq-install>` for more
information on supported Python versions and the 3.0 transition), but we recommend installing Python 2.5 or later. If you do so, you won't need to set up a database just yet: Python 2.5 or later includes a lightweight database called SQLite_.
diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt
index 69c8d0f3db..afda1f28a2 100644
--- a/docs/intro/tutorial01.txt
+++ b/docs/intro/tutorial01.txt
@@ -281,6 +281,7 @@ That'll create a directory :file:`polls`, which is laid out like this::
polls/
__init__.py
models.py
+ tests.py
views.py
This directory structure will house the poll application.
diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt
index 203c945c02..ad1bd9d990 100644
--- a/docs/intro/tutorial02.txt
+++ b/docs/intro/tutorial02.txt
@@ -34,11 +34,11 @@ activate the admin site for your installation, do these three things:
* Run ``python manage.py syncdb``. Since you have added a new application
to :setting:`INSTALLED_APPS`, the database tables need to be updated.
- * Edit your ``mysite/urls.py`` file and uncomment the lines below the
- "Uncomment the next two lines..." comment. This file is a URLconf;
- we'll dig into URLconfs in the next tutorial. For now, all you need to
- know is that it maps URL roots to applications. In the end, you should
- have a ``urls.py`` file that looks like this:
+ * Edit your ``mysite/urls.py`` file and uncomment the lines that reference
+ the admin -- there are three lines in total to uncomment. This file is a
+ URLconf; we'll dig into URLconfs in the next tutorial. For now, all you
+ need to know is that it maps URL roots to applications. In the end, you
+ should have a ``urls.py`` file that looks like this:
.. versionchanged:: 1.1
The method for adding admin urls has changed in Django 1.1.
diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt
index 238dc63f71..1438a9e776 100644
--- a/docs/intro/tutorial03.txt
+++ b/docs/intro/tutorial03.txt
@@ -171,15 +171,23 @@ and put the following Python code in it::
This is the simplest view possible. Go to "/polls/" in your browser, and you
should see your text.
-Now add the following view. It's slightly different, because it takes an
-argument (which, remember, is passed in from whatever was captured by the
-regular expression in the URLconf)::
+Now lets add a few more views. These views are slightly different, because
+they take an argument (which, remember, is passed in from whatever was
+captured by the regular expression in the URLconf)::
def detail(request, poll_id):
return HttpResponse("You're looking at poll %s." % poll_id)
-Take a look in your browser, at "/polls/34/". It'll display whatever ID you
-provide in the URL.
+ def results(request, poll_id):
+ return HttpResponse("You're looking at the results of poll %s." % poll_id)
+
+ def vote(request, poll_id):
+ return HttpResponse("You're voting on poll %s." % poll_id)
+
+Take a look in your browser, at "/polls/34/". It'll run the `detail()` method
+and display whatever ID you provide in the URL. Try "/polls/34/results/" and
+"/polls/34/vote/" too -- these will display the placeholder results and voting
+pages.
Write views that actually do something
======================================
@@ -467,10 +475,10 @@ Copy the file ``mysite/urls.py`` to ``mysite/polls/urls.py``. Then, change
``mysite/urls.py`` to remove the poll-specific URLs and insert an
:func:`~django.conf.urls.defaults.include`::
- ...
+ # ...
urlpatterns = patterns('',
(r'^polls/', include('mysite.polls.urls')),
- ...
+ # ...
:func:`~django.conf.urls.defaults.include`, simply, references another URLconf.
Note that the regular expression doesn't have a ``$`` (end-of-string match
diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt
index 28ace85ca8..394fc25ea8 100644
--- a/docs/intro/tutorial04.txt
+++ b/docs/intro/tutorial04.txt
@@ -21,6 +21,7 @@ tutorial, so that the template contains an HTML ``<form>`` element:
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="/polls/{{ poll.id }}/vote/" method="post">
+ {% csrf_token %}
{% for choice in poll.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
<label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
@@ -46,17 +47,41 @@ A quick rundown:
* ``forloop.counter`` indicates how many times the :ttag:`for` tag has gone
through its loop
+ * Since we are creating a POST form (which can have the effect of modifying
+ data), we unfortunately need to worry about Cross Site Request Forgeries.
+ Thankfully, you don't have to worry too hard, because Django comes with
+ very easy-to-use system for protecting against it. In short, all POST
+ forms that are targetted at internal URLs need the ``{% csrf_token %}``
+ template tag adding.
+
+The ``{% csrf_token %}`` tag requires information from the request object, which
+is not normally accessible from within the template context. To fix this, a
+small adjustment needs to be made to the ``detail`` view, so that it looks like
+the following::
+
+ from django.template import RequestContext
+ # ...
+ def detail(request, poll_id):
+ p = get_object_or_404(Poll, pk=poll_id)
+ return render_to_response('polls/detail.html', {'poll': p},
+ context_instance=RequestContext(request))
+
+The details of how this works are explained in the documentation for
+:ref:`RequestContext <subclassing-context-requestcontext>`.
+
Now, let's create a Django view that handles the submitted data and does
something with it. Remember, in :ref:`Tutorial 3 <intro-tutorial03>`, we
created a URLconf for the polls application that includes this line::
(r'^(?P<poll_id>\d+)/vote/$', 'vote'),
-So let's create a ``vote()`` function in ``mysite/polls/views.py``::
+We also created a dummy implementation of the ``vote()`` function. Let's
+create a real version. Add the following to ``mysite/polls/views.py``::
from django.shortcuts import get_object_or_404, render_to_response
- from django.http import HttpResponseRedirect
+ from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse
+ from django.template import RequestContext
from mysite.polls.models import Choice, Poll
# ...
def vote(request, poll_id):
@@ -68,7 +93,7 @@ So let's create a ``vote()`` function in ``mysite/polls/views.py``::
return render_to_response('polls/detail.html', {
'poll': p,
'error_message': "You didn't select a choice.",
- })
+ }, context_instance=RequestContext(request))
else:
selected_choice.votes += 1
selected_choice.save()
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index c1e05eda1d..0f746bf01b 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -770,7 +770,7 @@ documented in :ref:`topics-http-urls`::
However, the ``self.my_view`` function registered above suffers from two
problems:
- * It will *not* perform and permission checks, so it will be accessible to
+ * It will *not* perform any permission checks, so it will be accessible to
the general public.
* It will *not* provide any header details to prevent caching. This means if
the page retrieves data from the database, and caching middleware is
@@ -1048,16 +1048,70 @@ automatically::
FriendshipInline,
]
+Working with Many-to-Many Models
+--------------------------------
+
+.. versionadded:: 1.2
+
+By default, admin widgets for many-to-many relations will be displayed
+on whichever model contains the actual reference to the ``ManyToManyField``.
+Depending on your ``ModelAdmin`` definition, each many-to-many field in your
+model will be represented by a standard HTML ``<select multiple>``, a
+horizontal or vertical filter, or a ``raw_id_admin`` widget. However, it is
+also possible to to replace these widgets with inlines.
+
+Suppose we have the following models::
+
+ class Person(models.Model):
+ name = models.CharField(max_length=128)
+
+ class Group(models.Model):
+ name = models.CharField(max_length=128)
+ members = models.ManyToManyField(Person, related_name='groups')
+
+If you want to display many-to-many relations using an inline, you can do
+so by defining an ``InlineModelAdmin`` object for the relationship::
+
+ class MembershipInline(admin.TabularInline):
+ model = Group.members.through
+
+ class PersonAdmin(admin.ModelAdmin):
+ inlines = [
+ MembershipInline,
+ ]
+
+ class GroupAdmin(admin.ModelAdmin):
+ inlines = [
+ MembershipInline,
+ ]
+ exclude = ('members',)
+
+There are two features worth noting in this example.
+
+Firstly - the ``MembershipInline`` class references ``Group.members.through``.
+The ``through`` attribute is a reference to the model that manages the
+many-to-many relation. This model is automatically created by Django when you
+define a many-to-many field.
+
+Secondly, the ``GroupAdmin`` must manually exclude the ``members`` field.
+Django displays an admin widget for a many-to-many field on the model that
+defines the relation (in this case, ``Group``). If you want to use an inline
+model to represent the many-to-many relationship, you must tell Django's admin
+to *not* display this widget - otherwise you will end up with two widgets on
+your admin page for managing the relation.
+
+In all other respects, the ``InlineModelAdmin`` is exactly the same as any
+other. You can customize the appearance using any of the normal
+``InlineModelAdmin`` properties.
+
Working with Many-to-Many Intermediary Models
----------------------------------------------
-By default, admin widgets for many-to-many relations will be displayed inline
-on whichever model contains the actual reference to the ``ManyToManyField``.
-However, when you specify an intermediary model using the ``through``
-argument to a ``ManyToManyField``, the admin will not display a widget by
-default. This is because each instance of that intermediary model requires
-more information than could be displayed in a single widget, and the layout
-required for multiple widgets will vary depending on the intermediate model.
+When you specify an intermediary model using the ``through`` argument to a
+``ManyToManyField``, the admin will not display a widget by default. This is
+because each instance of that intermediary model requires more information
+than could be displayed in a single widget, and the layout required for
+multiple widgets will vary depending on the intermediate model.
However, we still want to be able to edit that information inline. Fortunately,
this is easy to do with inline admin models. Suppose we have the following
diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt
index 880be34101..6ee109782f 100644
--- a/docs/ref/contrib/comments/index.txt
+++ b/docs/ref/contrib/comments/index.txt
@@ -216,6 +216,13 @@ should know about:
it with a warning field; if you use the comment form with a custom
template you should be sure to do the same.
+The comments app also depends on the more general :ref:`Cross Site Request
+Forgery protection < ref-contrib-csrf>` that comes with Django. As described in
+the documentation, it is best to use ``CsrfViewMiddleware``. However, if you
+are not using that, you will need to use the ``csrf_protect`` decorator on any
+views that include the comment form, in order for those views to be able to
+output the CSRF token and cookie.
+
.. _honeypot: http://en.wikipedia.org/wiki/Honeypot_(computing)
More information
diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt
index 1b6b6102de..c1bdb59cd1 100644
--- a/docs/ref/contrib/csrf.txt
+++ b/docs/ref/contrib/csrf.txt
@@ -4,121 +4,421 @@
Cross Site Request Forgery protection
=====================================
-.. module:: django.contrib.csrf
+.. module:: django.middleware.csrf
:synopsis: Protects against Cross Site Request Forgeries
-The CsrfMiddleware class provides easy-to-use protection against
+The CSRF middleware and template tag provides easy-to-use protection against
`Cross Site Request Forgeries`_. This type of attack occurs when a malicious
-Web site creates a link or form button that is intended to perform some action
-on your Web site, using the credentials of a logged-in user who is tricked
-into clicking on the link in their browser.
+Web site contains a link, a form button or some javascript that is intended to
+perform some action on your Web site, using the credentials of a logged-in user
+who visits the malicious site in their browser. A related type of attack,
+'login CSRF', where an attacking site tricks a user's browser into logging into
+a site with someone else's credentials, is also covered.
-The first defense against CSRF attacks is to ensure that GET requests
-are side-effect free. POST requests can then be protected by adding this
-middleware into your list of installed middleware.
+The first defense against CSRF attacks is to ensure that GET requests are
+side-effect free. POST requests can then be protected by following the steps
+below.
+
+.. versionadded:: 1.2
+ The 'contrib' apps, including the admin, use the functionality described
+ here. Because it is security related, a few things have been added to core
+ functionality to allow this to happen without any required upgrade steps.
.. _Cross Site Request Forgeries: http://www.squarefree.com/securitytips/web-developers.html#CSRF
How to use it
=============
-Add the middleware ``'django.contrib.csrf.middleware.CsrfMiddleware'`` to
-your list of middleware classes, :setting:`MIDDLEWARE_CLASSES`. It needs to process
-the response after the SessionMiddleware, so must come before it in the
-list. It also must process the response before things like compression
-happen to the response, so it must come after GZipMiddleware in the
-list.
+.. versionchanged:: 1.2
+ The template tag functionality (the recommended way to use this) was added
+ in version 1.2. The previous method (still available) is described under
+ `Legacy method`_.
+
+To enable CSRF protection for your views, follow these steps:
+
+ 1. Add the middleware
+ ``'django.middleware.csrf.CsrfViewMiddleware'`` to your list of
+ middleware classes, :setting:`MIDDLEWARE_CLASSES`. (It should come
+ before ``CsrfResponseMiddleware`` if that is being used, and before any
+ view middleware that assume that CSRF attacks have been dealt with.)
+
+ Alternatively, you can use the decorator
+ ``django.views.decorators.csrf.csrf_protect`` on particular views you
+ want to protect (see below).
+
+ 2. In any template that uses a POST form, use the ``csrf_token`` tag inside
+ the ``<form>`` element if the form is for an internal URL, e.g.::
+
+ <form action="" method="POST">{% csrf_token %}
+
+ This should not be done for POST forms that target external URLs, since
+ that would cause the CSRF token to be leaked, leading to a vulnerability.
+
+ 3. In the corresponding view functions, ensure that the
+ ``'django.core.context_processors.csrf'`` context processor is
+ being used. Usually, this can be done in one of two ways:
+
+ 1. Use RequestContext, which always uses
+ ``'django.core.context_processors.csrf'`` (no matter what your
+ TEMPLATE_CONTEXT_PROCESSORS setting). If you are using
+ generic views or contrib apps, you are covered already, since these
+ apps use RequestContext throughout.
+
+ 2. Manually import and use the processor to generate the CSRF token and
+ add it to the template context. e.g.::
+
+ from django.core.context_processors import csrf
+ from django.shortcuts import render_to_response
+
+ def my_view(request):
+ c = {}
+ c.update(csrf(request))
+ # ... view code here
+ return render_to_response("a_template.html", c)
+
+ You may want to write your own ``render_to_response`` wrapper that
+ takes care of this step for you.
+
+The utility script ``extras/csrf_migration_helper.py`` can help to automate the
+finding of code and templates that may need to be upgraded. It contains full
+help on how to use it.
+
+The decorator method
+--------------------
+
+Rather than adding ``CsrfViewMiddleware`` as a blanket protection, you can use
+the ``csrf_protect`` decorator, which has exactly the same functionality, on
+particular views that need the protection. It must be used **both** on views
+that insert the CSRF token in the output, and on those that accept the POST form
+data. (These are often the same view function, but not always). It is used like
+this::
+
+ from django.views.decorators.csrf import csrf_protect
+ from django.template import RequestContext
+
+ @csrf_protect
+ def my_view(request):
+ c = {}
+ # ...
+ return render_to_response("a_template.html", c,
+ context_instance=RequestContext(request))
+
+Use of the decorator is **not recommended** by itself, since if you forget to
+use it, you will have a security hole. The 'belt and braces' strategy of using
+both is fine, and will incur minimal overhead.
+
+Legacy method
+-------------
+
+In Django 1.1, the template tag did not exist. Instead, a post-processing
+middleware that re-wrote POST forms to include the CSRF token was used. If you
+are upgrading a site from version 1.1 or earlier, please read this section and
+the `Upgrading notes`_ below. The post-processing middleware is still available
+as ``CsrfResponseMiddleware``, and it can be used by following these steps:
+
+ 1. Follow step 1 above to install ``CsrfViewMiddleware``.
+
+ 2. Add ``'django.middleware.csrf.CsrfResponseMiddleware'`` to your
+ :setting:`MIDDLEWARE_CLASSES` setting.
+
+ ``CsrfResponseMiddleware`` needs to process the response before things
+ like compression or setting ofETags happen to the response, so it must
+ come after ``GZipMiddleware``, ``CommonMiddleware`` and
+ ``ConditionalGetMiddleware`` in the list. It also must come after
+ ``CsrfViewMiddleware``.
+
+Use of the ``CsrfResponseMiddleware`` is not recommended because of the
+performance hit it imposes, and because of a potential security problem (see
+below). It can be used as an interim measure until applications have been
+updated to use the ``{% csrf_token %}`` tag. It is deprecated and will be
+removed in Django 1.4.
+
+Django 1.1 and earlier provided a single ``CsrfMiddleware`` class. This is also
+still available for backwards compatibility. It combines the functions of the
+two middleware.
+
+Note also that previous versions of these classes depended on the sessions
+framework, but this dependency has now been removed, with backward compatibility
+support so that upgrading will not produce any issues.
+
+Security of legacy method
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The post-processing ``CsrfResponseMiddleware`` adds the CSRF token to all POST
+forms (unless the view has been decorated with ``csrf_response_exempt``). If
+the POST form has an external untrusted site as its target, rather than an
+internal page, that site will be sent the CSRF token when the form is submitted.
+Armed with this leaked information, that site will then be able to successfully
+launch a CSRF attack on your site against that user. The
+``@csrf_response_exempt`` decorator can be used to fix this, but only if the
+page doesn't also contain internal forms that require the token.
+
+Upgrading notes
+---------------
+
+When upgrading to version 1.2 or later, you may have applications that rely on
+the old post-processing functionality for CSRF protection, or you may not have
+enabled any CSRF protection. This section outlines the steps necessary for a
+smooth upgrade, without having to fix all the applications to use the new
+template tag method immediately.
+
+First of all, the location of the middleware and related functions have
+changed. There are backwards compatible stub files so that old imports will
+continue to work for now, but they are deprecated and will be removed in Django
+1.4. The following changes have been made:
+
+ * Middleware have been moved to ``django.middleware.csrf``
+ * Decorators have been moved to ``django.views.decorators.csrf``
-The ``CsrfMiddleware`` class is actually composed of two middleware:
-``CsrfViewMiddleware`` which performs the checks on incoming requests,
-and ``CsrfResponseMiddleware`` which performs post-processing of the
-result. This allows the individual components to be used and/or
-replaced instead of using ``CsrfMiddleware``.
+====================================================== ==============================================
+ Old New
+====================================================== ==============================================
+django.contrib.csrf.middleware.CsrfMiddleware django.middleware.csrf.CsrfMiddleware
+django.contrib.csrf.middleware.CsrfViewMiddleware django.middleware.csrf.CsrfViewMiddleware
+django.contrib.csrf.middleware.CsrfResponseMiddleware django.middleware.csrf.CsrfResponseMiddleware
+django.contrib.csrf.middleware.csrf_exempt django.views.decorators.csrf_exempt
+django.contrib.csrf.middleware.csrf_view_exempt django.views.decorators.csrf_view_exempt
+django.contrib.csrf.middleware.csrf_response_exempt django.views.decorators.csrf_response_exempt
+====================================================== ==============================================
-.. versionchanged:: 1.1
- (previous versions of Django did not provide these two components
- of ``CsrfMiddleware`` as described above)
+You should update any imports, and also the paths in your
+:setting:`MIDDLEWARE_CLASSES`.
+
+If you have ``CsrfMiddleware`` in your :setting:`MIDDLEWARE_CLASSES`, you will now
+have a working installation with CSRF protection. It is recommended at this
+point that you replace ``CsrfMiddleware`` with its two components,
+``CsrfViewMiddleware`` and ``CsrfResponseMiddleware`` (in that order).
+
+If you do not have any of the middleware in your :setting:`MIDDLEWARE_CLASSES`,
+you will have a working installation but without any CSRF protection for your
+views (just as you had before). It is strongly recommended to install
+``CsrfViewMiddleware`` and ``CsrfResponseMiddleware``, as described above.
+
+Note that contrib apps, such as the admin, have been updated to use the
+``csrf_protect`` decorator, so that they are secured even if you do not add the
+``CsrfViewMiddleware`` to your settings. However, if you have supplied
+customised templates to any of the view functions of contrib apps (whether
+explicitly via a keyword argument, or by overriding built-in templates), **you
+MUST update them** to include the ``csrf_token`` template tag as described
+above, or they will stop working. (If you cannot update these templates for
+some reason, you will be forced to use ``CsrfResponseMiddleware`` for these
+views to continue working).
+
+Note also, if you are using the comments app, and you are not going to add
+``CsrfViewMiddleware`` to your settings (not recommended), you will need to add
+the ``csrf_protect`` decorator to any views that include the comment forms and
+target the comment views (usually using the :ttag:`comment_form_target` template
+tag).
+
+Assuming you have followed the above, all views in your Django site will now be
+protected by the ``CsrfViewMiddleware``. Contrib apps meet the requirements
+imposed by the ``CsrfViewMiddleware`` using the template tag, and other
+applications in your project will meet its requirements by virtue of the
+``CsrfResponseMiddleware``.
+
+The next step is to update all your applications to use the template tag, as
+described in `How to use it`_, steps 2-3. This can be done as soon as is
+practical. Any applications that are updated will now require Django 1.1.2 or
+later, since they will use the CSRF template tag which was not available in
+earlier versions. (The template tag in 1.1.2 is actually a no-op that exists
+solely to ease the transition to 1.2 — it allows apps to be created that have
+CSRF protection under 1.2 without requiring users of the apps to upgrade to the
+Django 1.2.X series).
+
+The utility script ``extras/csrf_migration_helper.py`` can help to automate the
+finding of code and templates that may need to be upgraded. It contains full
+help on how to use it.
+
+Finally, once all applications are upgraded, ``CsrfResponseMiddleware`` can be
+removed from your settings.
+
+While ``CsrfResponseMiddleware`` is still in use, the ``csrf_response_exempt``
+decorator, described in `Exceptions`_, may be useful. The post-processing
+middleware imposes a performance hit and a potential vulnerability, and any
+views that have been upgraded to use the new template tag method no longer need
+it.
Exceptions
----------
.. versionadded:: 1.1
-To manually exclude a view function from being handled by the
-CsrfMiddleware, you can use the ``csrf_exempt`` decorator, found in
-the ``django.contrib.csrf.middleware`` module. For example::
+To manually exclude a view function from being handled by either of the two CSRF
+middleware, you can use the ``csrf_exempt`` decorator, found in the
+``django.views.decorators.csrf`` module. For example::
- from django.contrib.csrf.middleware import csrf_exempt
+ from django.views.decorators.csrf import csrf_exempt
+ @csrf_exempt
def my_view(request):
return HttpResponse('Hello world')
- my_view = csrf_exempt(my_view)
-Like the middleware itself, the ``csrf_exempt`` decorator is composed
-of two parts: a ``csrf_view_exempt`` decorator and a
-``csrf_response_exempt`` decorator, found in the same module. These
-disable the view protection mechanism (``CsrfViewMiddleware``) and the
-response post-processing (``CsrfResponseMiddleware``) respectively.
-They can be used individually if required.
+Like the middleware, the ``csrf_exempt`` decorator is composed of two parts: a
+``csrf_view_exempt`` decorator and a ``csrf_response_exempt`` decorator, found
+in the same module. These disable the view protection mechanism
+(``CsrfViewMiddleware``) and the response post-processing
+(``CsrfResponseMiddleware``) respectively. They can be used individually if
+required.
+
+You don't have to worry about doing this for most AJAX views. Any request sent
+with "X-Requested-With: XMLHttpRequest" is automatically exempt. (See the `How
+it works`_ section.)
+
+Subdomains
+----------
+
+By default, CSRF cookies are specific to the subdomain they are set for. This
+means that a form served from one subdomain (e.g. server1.example.com) will not
+be able to have a target on another subdomain (e.g. server2.example.com). This
+restriction can be removed by setting :setting:`CSRF_COOKIE_DOMAIN` to be
+something like ``".example.com"``.
+
+Please note that, with or without use of this setting, this CSRF protection
+mechanism is not safe against cross-subdomain attacks -- see `Limitations`_.
+
+Rejected requests
+=================
+
+By default, a '403 Forbidden' response is sent to the user if an incoming
+request fails the checks performed by ``CsrfViewMiddleware``. This should
+usually only be seen when there is a genuine Cross Site Request Forgery, or
+when, due to a programming error, the CSRF token has not been included with a
+POST form.
+
+No logging is done, and the error message is not very friendly, so you may want
+to provide your own page for handling this condition. To do this, simply set
+the :setting:`CSRF_FAILURE_VIEW` setting to a dotted path to your own view
+function, which should have the following signature::
-You don't have to worry about doing this for most AJAX views. Any
-request sent with "X-Requested-With: XMLHttpRequest" is automatically
-exempt. (See the next section.)
+ def csrf_failure(request, reason="")
+
+where ``reason`` is a short message (intended for developers or logging, not for
+end users) indicating the reason the request was rejected.
How it works
============
-CsrfMiddleware does two things:
+The CSRF protection is based on the following things:
+
+1. A CSRF cookie that is set to a random value (a session independent nonce, as
+ it is called), which other sites will not have access to.
+
+ This cookie is set by ``CsrfViewMiddleware``. It is meant to be permanent,
+ but since there is no way to set a cookie that never expires, it is sent with
+ every response that has called ``django.middleware.csrf.get_token()``
+ (the function used internally to retrieve the CSRF token).
+
+2. A hidden form field with the name 'csrfmiddlewaretoken' present in all
+ outgoing POST forms. The value of this field is the value of the CSRF
+ cookie.
-1. It modifies outgoing requests by adding a hidden form field to all
- 'POST' forms, with the name 'csrfmiddlewaretoken' and a value which is
- a hash of the session ID plus a secret. If there is no session ID set,
- this modification of the response isn't done, so there is very little
- performance penalty for those requests that don't have a session.
- (This is done by ``CsrfResponseMiddleware``).
+ This part is done by the template tag (and with the legacy method, it is done
+ by ``CsrfResponseMiddleware``).
-2. On all incoming POST requests that have the session cookie set, it
- checks that the 'csrfmiddlewaretoken' is present and correct. If it
- isn't, the user will get a 403 error. (This is done by
- ``CsrfViewMiddleware``)
+3. For all incoming POST requests, a CSRF cookie must be present, and the
+ 'csrfmiddlewaretoken' field must be present and correct. If it isn't, the
+ user will get a 403 error.
-This ensures that only forms that have originated from your Web site
-can be used to POST data back.
+ This check is done by ``CsrfViewMiddleware``.
+
+4. In addition, for HTTPS requests, strict referer checking is done by
+ ``CsrfViewMiddleware``. This is necessary to address a Man-In-The-Middle
+ attack that is possible under HTTPS when using a session independent nonce,
+ due to the fact that HTTP 'Set-Cookie' headers are (unfortunately) accepted
+ by clients that are talking to a site under HTTPS. (Referer checking is not
+ done for HTTP requests because the presence of the Referer header is not
+ reliable enough under HTTP.)
+
+This ensures that only forms that have originated from your Web site can be used
+to POST data back.
It deliberately only targets HTTP POST requests (and the corresponding POST
-forms). GET requests ought never to have any potentially dangerous side
-effects (see `9.1.1 Safe Methods, HTTP 1.1, RFC 2616`_), and so a
-CSRF attack with a GET request ought to be harmless.
+forms). GET requests ought never to have any potentially dangerous side effects
+(see `9.1.1 Safe Methods, HTTP 1.1, RFC 2616`_), and so a CSRF attack with a GET
+request ought to be harmless.
-POST requests that are not accompanied by a session cookie are not protected,
-but they do not need to be protected, since the 'attacking' Web site
-could make these kind of requests anyway.
+``CsrfResponseMiddleware`` checks the Content-Type before modifying the
+response, and only pages that are served as 'text/html' or
+'application/xml+xhtml' are modified.
-The Content-Type is checked before modifying the response, and only
-pages that are served as 'text/html' or 'application/xml+xhtml'
-are modified.
+AJAX
+----
-The middleware tries to be smart about requests that come in via AJAX. Many
-JavaScript toolkits send an "X-Requested-With: XMLHttpRequest" HTTP header;
-these requests are detected and automatically *not* handled by this middleware.
-We can do this safely because, in the context of a browser, the header can only
-be added by using ``XMLHttpRequest``, and browsers already implement a
-same-domain policy for ``XMLHttpRequest``. (Note that this is not secure if you
-don't trust content within the same domain or subdomains.)
+The middleware tries to be smart about requests that come in via AJAX. Most
+modern JavaScript toolkits send an "X-Requested-With: XMLHttpRequest" HTTP
+header; these requests are detected and automatically *not* handled by this
+middleware. We can do this safely because, in the context of a browser, the
+header can only be added by using ``XMLHttpRequest``, and browsers already
+implement a same-domain policy for ``XMLHttpRequest``.
+For the more recent browsers that relax this same-domain policy, custom headers
+like "X-Requested-With" are only allowed after the browser has done a
+'preflight' check to the server to see if the cross-domain request is allowed,
+using a strictly 'opt in' mechanism, so the exception for AJAX is still safe—if
+the developer has specifically opted in to allowing cross-site AJAX POST
+requests on a specific URL, they obviously don't want the middleware to disallow
+exactly that.
.. _9.1.1 Safe Methods, HTTP 1.1, RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
+Caching
+=======
+
+If the ``csrf_token`` template tag is used by a template (or the ``get_token``
+function is called some other way), ``CsrfViewMiddleware`` will add a cookie and
+a ``Vary: Cookie`` header to the response. Similarly,
+``CsrfResponseMiddleware`` will send the ``Vary: Cookie`` header if it inserted
+a token. This means that these middleware will play well with the cache
+middleware if it is used as instructed (``UpdateCacheMiddleware`` goes before
+all other middleware).
+
+However, if you use cache decorators on individual views, the CSRF middleware
+will not yet have been able to set the Vary header. In this case, on any views
+that will require a CSRF token to be inserted you should use the
+:func:`django.views.decorators.vary.vary_on_cookie` decorator first::
+
+ from django.views.decorators.cache import cache_page
+ from django.views.decorators.vary import vary_on_cookie
+
+ @cache_page(60 * 15)
+ @vary_on_cookie
+ def my_view(request):
+ # ...
+
+
+Testing
+=======
+
+The ``CsrfViewMiddleware`` will usually be a big hindrance to testing view
+functions, due to the need for the CSRF token which must be sent with every POST
+request. For this reason, Django's HTTP client for tests has been modified to
+set a flag on requests which relaxes the middleware and the ``csrf_protect``
+decorator so that they no longer rejects requests. In every other respect
+(e.g. sending cookies etc.), they behave the same.
+
Limitations
===========
-CsrfMiddleware requires Django's session framework to work. If you have
-a custom authentication system that manually sets cookies and the like,
-it won't help you.
+Subdomains within a site will be able to set cookies on the client for the whole
+domain. By setting the cookie and using a corresponding token, subdomains will
+be able to circumvent the CSRF protection. The only way to avoid this is to
+ensure that subdomains are controlled by trusted users (or, are at least unable
+to set cookies). Note that even without CSRF, there are other vulnerabilities,
+such as session fixation, that make giving subdomains to untrusted parties a bad
+idea, and these vulnerabilities cannot easily be fixed with current browsers.
+
+If you are using ``CsrfResponseMiddleware`` and your app creates HTML pages and
+forms in some unusual way, (e.g. it sends fragments of HTML in JavaScript
+document.write statements) you might bypass the filter that adds the hidden
+field to the form, in which case form submission will always fail. You should
+use the template tag or :meth:`django.middleware.csrf.get_token` to get
+the CSRF token and ensure it is included when your form is submitted.
+
+Contrib and reusable apps
+=========================
-If your app creates HTML pages and forms in some unusual way, (e.g.
-it sends fragments of HTML in JavaScript document.write statements)
-you might bypass the filter that adds the hidden field to the form,
-in which case form submission will always fail. It may still be possible
-to use the middleware, provided you can find some way to get the
-CSRF token and ensure that is included when your form is submitted.
+Because it is possible for the developer to turn off the ``CsrfViewMiddleware``,
+all relevant views in contrib apps use the ``csrf_protect`` decorator to ensure
+the security of these applications against CSRF. It is recommended that the
+developers of other reusable apps that want the same guarantees also use the
+``csrf_protect`` decorator on their views.
diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt
index 98f0dbad42..11a2aed091 100644
--- a/docs/ref/contrib/formtools/form-wizard.txt
+++ b/docs/ref/contrib/formtools/form-wizard.txt
@@ -177,7 +177,7 @@ Here's a full example template:
{% block content %}
<p>Step {{ step }} of {{ step_count }}</p>
- <form action="." method="post">
+ <form action="." method="post">{% csrf_token %}
<table>
{{ form }}
</table>
diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt
index ff51df9e8f..b0b26cc227 100644
--- a/docs/ref/middleware.txt
+++ b/docs/ref/middleware.txt
@@ -165,11 +165,11 @@ every incoming ``HttpRequest`` object. See :ref:`Authentication in Web requests
CSRF protection middleware
--------------------------
-.. module:: django.contrib.csrf.middleware
+.. module:: django.middleware.csrf
:synopsis: Middleware adding protection against Cross Site Request
Forgeries.
-.. class:: django.contrib.csrf.middleware.CsrfMiddleware
+.. class:: django.middleware.csrf.CsrfMiddleware
.. versionadded:: 1.0
diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt
index efd7c549b8..e685472cca 100644
--- a/docs/ref/models/querysets.txt
+++ b/docs/ref/models/querysets.txt
@@ -1114,6 +1114,17 @@ Aggregation <topics-db-aggregation>`.
.. _field-lookups:
+``exists()``
+~~~~~~~~~~~~
+
+.. versionadded:: 1.2
+
+Returns ``True`` if the :class:`QuerySet` contains any results, and ``False``
+if not. This tries to perform the query in the simplest and fastest way
+possible, but it *does* execute nearly the same query. This means that calling
+:meth:`QuerySet.exists()` is faster that ``bool(some_query_set)``, but not by
+a large degree.
+
Field lookups
-------------
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index e8c673d995..6e78030e8c 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -144,6 +144,51 @@ Default: ``600``
The default number of seconds to cache a page when the caching middleware or
``cache_page()`` decorator is used.
+.. setting:: CSRF_COOKIE_NAME
+
+CSRF_COOKIE_NAME
+----------------
+
+.. versionadded:: 1.2
+
+Default: ``'csrftoken'``
+
+The name of the cookie to use for the CSRF authentication token. This can be whatever you
+want. See :ref:`ref-contrib-csrf`.
+
+.. setting:: CSRF_COOKIE_DOMAIN
+
+CSRF_COOKIE_DOMAIN
+------------------
+
+.. versionadded:: 1.2
+
+Default: ``None``
+
+The domain to be used when setting the CSRF cookie. This can be useful for
+allowing cross-subdomain requests to be exluded from the normal cross site
+request forgery protection. It should be set to a string such as
+``".lawrence.com"`` to allow a POST request from a form on one subdomain to be
+accepted by accepted by a view served from another subdomain.
+
+.. setting:: CSRF_FAILURE_VIEW
+
+CSRF_FAILURE_VIEW
+-----------------
+
+.. versionadded:: 1.2
+
+Default: ``'django.views.csrf.csrf_failure'``
+
+A dotted path to the view function to be used when an incoming request
+is rejected by the CSRF protection. The function should have this signature::
+
+ def csrf_failure(request, reason="")
+
+where ``reason`` is a short message (intended for developers or logging, not for
+end users) indicating the reason the request was rejected. See
+:ref:`ref-contrib-csrf`.
+
.. setting:: DATABASE_ENGINE
DATABASE_ENGINE
@@ -379,6 +424,29 @@ are not allowed to visit any page, systemwide. Use this for bad robots/crawlers.
This is only used if ``CommonMiddleware`` is installed (see
:ref:`topics-http-middleware`).
+.. setting:: EMAIL_BACKEND
+
+EMAIL_BACKEND
+-------------
+
+.. versionadded:: 1.2
+
+Default: ``'django.core.mail.backends.smtp'``
+
+The backend to use for sending emails. For the list of available backends see
+:ref:`topics-email`.
+
+.. setting:: EMAIL_FILE_PATH
+
+EMAIL_FILE_PATH
+---------------
+
+.. versionadded:: 1.2
+
+Default: Not defined
+
+The directory used by the ``file`` email backend to store output files.
+
.. setting:: EMAIL_HOST
EMAIL_HOST
@@ -751,6 +819,7 @@ Default::
('django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',)
A tuple of middleware classes to use. See :ref:`topics-http-middleware`.
diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt
index e3260a96f8..1b6eeb7014 100644
--- a/docs/ref/templates/api.txt
+++ b/docs/ref/templates/api.txt
@@ -313,6 +313,13 @@ and return a dictionary of items to be merged into the context. By default,
"django.core.context_processors.i18n",
"django.core.context_processors.media")
+.. versionadded:: 1.2
+ In addition to these, ``RequestContext`` always uses
+ ``'django.core.context_processors.csrf'``. This is a security
+ related context processor required by the admin and other contrib apps, and,
+ in case of accidental misconfiguration, it is deliberately hardcoded in and
+ cannot be turned off by the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting.
+
Each processor is applied in order. That means, if one processor adds a
variable to the context and a second processor adds a variable with the same
name, the second will override the first. The default processors are explained
@@ -404,6 +411,14 @@ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every
``RequestContext`` will contain a variable ``MEDIA_URL``, providing the
value of the :setting:`MEDIA_URL` setting.
+django.core.context_processors.csrf
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.2
+
+This processor adds a token that is needed by the ``csrf_token`` template tag
+for protection against :ref:`Cross Site Request Forgeries <ref-contrib-csrf>`.
+
django.core.context_processors.request
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index a2f8b9f8b3..8266224c39 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -53,6 +53,16 @@ Ignore everything between ``{% comment %}`` and ``{% endcomment %}``
.. templatetag:: cycle
+csrf_token
+~~~~~~~~~~
+
+.. versionadded:: 1.1.2
+
+In the Django 1.1.X series, this is a no-op tag that returns an empty string for
+future compatibility purposes. In Django 1.2 and later, it is used for CSRF
+protection, as described in the documentation for :ref:`Cross Site Request
+Forgeries <ref-contrib-csrf>`.
+
cycle
~~~~~
diff --git a/docs/releases/1.2-alpha.txt b/docs/releases/1.2-alpha.txt
new file mode 100644
index 0000000000..5a0815bf36
--- /dev/null
+++ b/docs/releases/1.2-alpha.txt
@@ -0,0 +1,52 @@
+
+Backwards-incompatible changes
+==============================
+
+CSRF Protection
+---------------
+
+There have been large changes to the way that CSRF protection works, detailed in
+:ref:`the CSRF documentaton <ref-contrib-csrf>`. The following are the major
+changes that developers must be aware of:
+
+ * ``CsrfResponseMiddleware`` and ``CsrfMiddleware`` have been deprecated, and
+ will be removed completely in Django 1.4, in favour of a template tag that
+ should be inserted into forms.
+
+ * All contrib apps use a ``csrf_protect`` decorator to protect the view. This
+ requires the use of the csrf_token template tag in the template, so if you
+ have used custom templates for contrib views, you MUST READ THE UPGRADE
+ INSTRUCTIONS to fix those templates.
+
+ * ``CsrfViewMiddleware`` is included in :setting:`MIDDLEWARE_CLASSES` by
+ default. This turns on CSRF protection by default, so that views that accept
+ POST requests need to be written to work with the middleware. Instructions
+ on how to do this are found in the CSRF docs.
+
+ * All of the CSRF has moved from contrib to core (with backwards compatible
+ imports in the old locations, which are deprecated).
+
+LazyObject
+----------
+
+``LazyObject`` is an undocumented utility class used for lazily wrapping other
+objects of unknown type. In Django 1.1 and earlier, it handled introspection in
+a non-standard way, depending on wrapped objects implementing a public method
+``get_all_members()``. Since this could easily lead to name clashes, it has been
+changed to use the standard method, involving ``__members__`` and ``__dir__()``.
+If you used ``LazyObject`` in your own code, and implemented the
+``get_all_members()`` method for wrapped objects, you need to make the following
+changes:
+
+ * If your class does not have special requirements for introspection (i.e. you
+ have not implemented ``__getattr__()`` or other methods that allow for
+ attributes not discoverable by normal mechanisms), you can simply remove the
+ ``get_all_members()`` method. The default implementation on ``LazyObject``
+ will do the right thing.
+
+ * If you have more complex requirements for introspection, first rename the
+ ``get_all_members()`` method to ``__dir__()``. This is the standard method,
+ from Python 2.6 onwards, for supporting introspection. If you are require
+ support for Python < 2.6, add the following code to the class::
+
+ __members__ = property(lambda self: self.__dir__())
diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt
index 6a62be8c32..33461a0858 100644
--- a/docs/topics/auth.txt
+++ b/docs/topics/auth.txt
@@ -262,8 +262,8 @@ Manager functions
Creates, saves and returns a :class:`~django.contrib.auth.models.User`.
The :attr:`~django.contrib.auth.models.User.username`,
:attr:`~django.contrib.auth.models.User.email` and
- :attr:`~django.contrib.auth.models.User.password` are set as given, and
- the :class:`~django.contrib.auth.models.User` gets ``is_active=True``.
+ :attr:`~django.contrib.auth.models.User.password` are set as given, and the
+ :class:`~django.contrib.auth.models.User` gets ``is_active=True``.
If no password is provided,
:meth:`~django.contrib.auth.models.User.set_unusable_password()` will
@@ -705,7 +705,7 @@ the following line to your URLconf::
(r'^accounts/login/$', 'django.contrib.auth.views.login'),
-.. function:: views.login(request, [template_name, redirect_field_name])
+.. function:: views.login(request, [template_name, redirect_field_name, authentication_form])
Here's what ``django.contrib.auth.views.login`` does:
@@ -767,7 +767,7 @@ the following line to your URLconf::
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
- <form method="post" action="{% url django.contrib.auth.views.login %}">
+ <form method="post" action="{% url django.contrib.auth.views.login %}">{% csrf_token %}
<table>
<tr>
<td>{{ form.username.label_tag }}</td>
@@ -785,6 +785,15 @@ the following line to your URLconf::
{% endblock %}
+ .. versionadded:: 1.2
+
+ If you are using alternate authentication (see
+ :ref:`authentication-backends`) you can pass a custom authentication form
+ to the login view via the ``authentication_form`` parameter. This form must
+ accept a ``request`` keyword argument in its ``__init__`` method, and
+ provide a ``get_user`` argument which returns the authenticated user object
+ (this method is only ever called after successful form validation).
+
.. _forms documentation: ../forms/
.. _site framework docs: ../sites/
@@ -824,7 +833,7 @@ includes a few other useful built-in views located in
* ``login_url``: The URL of the login page to redirect to. This will
default to :setting:`settings.LOGIN_URL <LOGIN_URL>` if not supplied.
-.. function:: views.password_change(request[, template_name, post_change_redirect])
+.. function:: views.password_change(request[, template_name, post_change_redirect, password_change_form])
Allows a user to change their password.
@@ -837,6 +846,13 @@ includes a few other useful built-in views located in
* ``post_change_redirect``: The URL to redirect to after a successful
password change.
+ * .. versionadded:: 1.2
+
+ ``password_change_form``: A custom "change password" form which must
+ accept a ``user`` keyword argument. The form is responsible for
+ actually changing the user's password.
+
+
**Template context:**
* ``form``: The password change form.
@@ -1030,15 +1046,7 @@ checks to make sure the user is logged in and has the permission
optional ``login_url`` argument, which lets you specify the URL for your
login page (:setting:`settings.LOGIN_URL <LOGIN_URL>` by default).
- Example in Python 2.3 syntax::
-
- from django.contrib.auth.decorators import user_passes_test
-
- def my_view(request):
- # ...
- my_view = user_passes_test(lambda u: u.has_perm('polls.can_vote'), login_url='/login/')(my_view)
-
- Example in Python 2.4 syntax::
+ For example::
from django.contrib.auth.decorators import user_passes_test
diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt
index c9fd1b4012..31900cd49f 100644
--- a/docs/topics/cache.txt
+++ b/docs/topics/cache.txt
@@ -616,12 +616,6 @@ like so::
from django.views.decorators.vary import vary_on_headers
- # Python 2.3 syntax.
- def my_view(request):
- # ...
- my_view = vary_on_headers(my_view, 'User-Agent')
-
- # Python 2.4+ decorator syntax.
@vary_on_headers('User-Agent')
def my_view(request):
# ...
diff --git a/docs/topics/conditional-view-processing.txt b/docs/topics/conditional-view-processing.txt
index 33f34b2406..53cb3205d8 100644
--- a/docs/topics/conditional-view-processing.txt
+++ b/docs/topics/conditional-view-processing.txt
@@ -95,13 +95,6 @@ for your front page view::
def front_page(request, blog_id):
...
-Of course, if you're using Python 2.3 or prefer not to use the decorator
-syntax, you can write the same code as follows, there is no difference::
-
- def front_page(request, blog_id):
- ...
- front_page = condition(last_modified_func=latest_entry)(front_page)
-
Shortcuts for only computing one value
======================================
diff --git a/docs/topics/email.txt b/docs/topics/email.txt
index c80a035b53..92e3c0263d 100644
--- a/docs/topics/email.txt
+++ b/docs/topics/email.txt
@@ -7,11 +7,13 @@ Sending e-mail
.. module:: django.core.mail
:synopsis: Helpers to easily send e-mail.
-Although Python makes sending e-mail relatively easy via the `smtplib library`_,
-Django provides a couple of light wrappers over it, to make sending e-mail
-extra quick.
+Although Python makes sending e-mail relatively easy via the `smtplib
+library`_, Django provides a couple of light wrappers over it. These wrappers
+are provided to make sending e-mail extra quick, to make it easy to test
+email sending during development, and to provide support for platforms that
+can't use SMTP.
-The code lives in a single module: ``django.core.mail``.
+The code lives in the ``django.core.mail`` module.
.. _smtplib library: http://docs.python.org/library/smtplib.html
@@ -25,11 +27,11 @@ In two lines::
send_mail('Subject here', 'Here is the message.', 'from@example.com',
['to@example.com'], fail_silently=False)
-Mail is sent using the SMTP host and port specified in the :setting:`EMAIL_HOST`
-and :setting:`EMAIL_PORT` settings. The :setting:`EMAIL_HOST_USER` and
-:setting:`EMAIL_HOST_PASSWORD` settings, if set, are used to authenticate to the
-SMTP server, and the :setting:`EMAIL_USE_TLS` setting controls whether a secure
-connection is used.
+Mail is sent using the SMTP host and port specified in the
+:setting:`EMAIL_HOST` and :setting:`EMAIL_PORT` settings. The
+:setting:`EMAIL_HOST_USER` and :setting:`EMAIL_HOST_PASSWORD` settings, if
+set, are used to authenticate to the SMTP server, and the
+:setting:`EMAIL_USE_TLS` setting controls whether a secure connection is used.
.. note::
@@ -42,7 +44,7 @@ send_mail()
The simplest way to send e-mail is using the function
``django.core.mail.send_mail()``. Here's its definition:
- .. function:: send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None)
+ .. function:: send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None, connection=None)
The ``subject``, ``message``, ``from_email`` and ``recipient_list`` parameters
are required.
@@ -62,6 +64,10 @@ are required.
* ``auth_password``: The optional password to use to authenticate to the
SMTP server. If this isn't provided, Django will use the value of the
``EMAIL_HOST_PASSWORD`` setting.
+ * ``connection``: The optional email backend to use to send the mail.
+ If unspecified, an instance of the default backend will be used.
+ See the documentation on :ref:`E-mail backends <topic-email-backends>`
+ for more details.
.. _smtplib docs: http://docs.python.org/library/smtplib.html
@@ -71,26 +77,29 @@ send_mass_mail()
``django.core.mail.send_mass_mail()`` is intended to handle mass e-mailing.
Here's the definition:
- .. function:: send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None)
+ .. function:: send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None, connection=None)
``datatuple`` is a tuple in which each element is in this format::
(subject, message, from_email, recipient_list)
``fail_silently``, ``auth_user`` and ``auth_password`` have the same functions
-as in ``send_mail()``.
+as in :meth:`~django.core.mail.send_mail()`.
Each separate element of ``datatuple`` results in a separate e-mail message.
-As in ``send_mail()``, recipients in the same ``recipient_list`` will all see
-the other addresses in the e-mail messages' "To:" field.
+As in :meth:`~django.core.mail.send_mail()`, recipients in the same
+``recipient_list`` will all see the other addresses in the e-mail messages'
+"To:" field.
send_mass_mail() vs. send_mail()
--------------------------------
-The main difference between ``send_mass_mail()`` and ``send_mail()`` is that
-``send_mail()`` opens a connection to the mail server each time it's executed,
-while ``send_mass_mail()`` uses a single connection for all of its messages.
-This makes ``send_mass_mail()`` slightly more efficient.
+The main difference between :meth:`~django.core.mail.send_mass_mail()` and
+:meth:`~django.core.mail.send_mail()` is that
+:meth:`~django.core.mail.send_mail()` opens a connection to the mail server
+each time it's executed, while :meth:`~django.core.mail.send_mass_mail()` uses
+a single connection for all of its messages. This makes
+:meth:`~django.core.mail.send_mass_mail()` slightly more efficient.
mail_admins()
=============
@@ -98,7 +107,7 @@ mail_admins()
``django.core.mail.mail_admins()`` is a shortcut for sending an e-mail to the
site admins, as defined in the :setting:`ADMINS` setting. Here's the definition:
- .. function:: mail_admins(subject, message, fail_silently=False)
+ .. function:: mail_admins(subject, message, fail_silently=False, connection=None)
``mail_admins()`` prefixes the subject with the value of the
:setting:`EMAIL_SUBJECT_PREFIX` setting, which is ``"[Django] "`` by default.
@@ -115,7 +124,7 @@ mail_managers() function
sends an e-mail to the site managers, as defined in the :setting:`MANAGERS`
setting. Here's the definition:
- .. function:: mail_managers(subject, message, fail_silently=False)
+ .. function:: mail_managers(subject, message, fail_silently=False, connection=None)
Examples
========
@@ -145,7 +154,7 @@ scripts generate.
The Django e-mail functions outlined above all protect against header injection
by forbidding newlines in header values. If any ``subject``, ``from_email`` or
``recipient_list`` contains a newline (in either Unix, Windows or Mac style),
-the e-mail function (e.g. ``send_mail()``) will raise
+the e-mail function (e.g. :meth:`~django.core.mail.send_mail()`) will raise
``django.core.mail.BadHeaderError`` (a subclass of ``ValueError``) and, hence,
will not send the e-mail. It's your responsibility to validate all data before
passing it to the e-mail functions.
@@ -178,41 +187,47 @@ from the request's POST data, sends that to admin@example.com and redirects to
.. _emailmessage-and-smtpconnection:
-The EmailMessage and SMTPConnection classes
-===========================================
+The EmailMessage class
+======================
.. versionadded:: 1.0
-Django's ``send_mail()`` and ``send_mass_mail()`` functions are actually thin
-wrappers that make use of the ``EmailMessage`` and ``SMTPConnection`` classes
-in ``django.core.mail``. If you ever need to customize the way Django sends
-e-mail, you can subclass these two classes to suit your needs.
+Django's :meth:`~django.core.mail.send_mail()` and
+:meth:`~django.core.mail.send_mass_mail()` functions are actually thin
+wrappers that make use of the :class:`~django.core.mail.EmailMessage` class.
+
+Not all features of the :class:`~django.core.mail.EmailMessage` class are
+available through the :meth:`~django.core.mail.send_mail()` and related
+wrapper functions. If you wish to use advanced features, such as BCC'ed
+recipients, file attachments, or multi-part e-mail, you'll need to create
+:class:`~django.core.mail.EmailMessage` instances directly.
.. note::
- Not all features of the ``EmailMessage`` class are available through the
- ``send_mail()`` and related wrapper functions. If you wish to use advanced
- features, such as BCC'ed recipients, file attachments, or multi-part
- e-mail, you'll need to create ``EmailMessage`` instances directly.
+ This is a design feature. :meth:`~django.core.mail.send_mail()` and
+ related functions were originally the only interface Django provided.
+ However, the list of parameters they accepted was slowly growing over
+ time. It made sense to move to a more object-oriented design for e-mail
+ messages and retain the original functions only for backwards
+ compatibility.
- This is a design feature. ``send_mail()`` and related functions were
- originally the only interface Django provided. However, the list of
- parameters they accepted was slowly growing over time. It made sense to
- move to a more object-oriented design for e-mail messages and retain the
- original functions only for backwards compatibility.
+:class:`~django.core.mail.EmailMessage` is responsible for creating the e-mail
+message itself. The :ref:`e-mail backend <topic-email-backends>` is then
+responsible for sending the e-mail.
-In general, ``EmailMessage`` is responsible for creating the e-mail message
-itself. ``SMTPConnection`` is responsible for the network connection side of
-the operation. This means you can reuse the same connection (an
-``SMTPConnection`` instance) for multiple messages.
+For convenience, :class:`~django.core.mail.EmailMessage` provides a simple
+``send()`` method for sending a single email. If you need to send multiple
+messages, the email backend API :ref:`provides an alternative
+<topics-sending-multiple-emails>`.
EmailMessage Objects
--------------------
.. class:: EmailMessage
-The ``EmailMessage`` class is initialized with the following parameters (in
-the given order, if positional arguments are used). All parameters are
-optional and can be set at any time prior to calling the ``send()`` method.
+The :class:`~django.core.mail.EmailMessage` class is initialized with the
+following parameters (in the given order, if positional arguments are used).
+All parameters are optional and can be set at any time prior to calling the
+``send()`` method.
* ``subject``: The subject line of the e-mail.
@@ -227,7 +242,7 @@ optional and can be set at any time prior to calling the ``send()`` method.
* ``bcc``: A list or tuple of addresses used in the "Bcc" header when
sending the e-mail.
- * ``connection``: An ``SMTPConnection`` instance. Use this parameter if
+ * ``connection``: An e-mail backend instance. Use this parameter if
you want to use the same connection for multiple messages. If omitted, a
new connection is created when ``send()`` is called.
@@ -248,18 +263,18 @@ For example::
The class has the following methods:
- * ``send(fail_silently=False)`` sends the message, using either
- the connection that is specified in the ``connection``
- attribute, or creating a new connection if none already
- exists. If the keyword argument ``fail_silently`` is ``True``,
- exceptions raised while sending the message will be quashed.
+ * ``send(fail_silently=False)`` sends the message. If a connection was
+ specified when the email was constructed, that connection will be used.
+ Otherwise, an instance of the default backend will be instantiated and
+ used. If the keyword argument ``fail_silently`` is ``True``, exceptions
+ raised while sending the message will be quashed.
* ``message()`` constructs a ``django.core.mail.SafeMIMEText`` object (a
subclass of Python's ``email.MIMEText.MIMEText`` class) or a
- ``django.core.mail.SafeMIMEMultipart`` object holding the
- message to be sent. If you ever need to extend the ``EmailMessage`` class,
- you'll probably want to override this method to put the content you want
- into the MIME object.
+ ``django.core.mail.SafeMIMEMultipart`` object holding the message to be
+ sent. If you ever need to extend the
+ :class:`~django.core.mail.EmailMessage` class, you'll probably want to
+ override this method to put the content you want into the MIME object.
* ``recipients()`` returns a list of all the recipients of the message,
whether they're recorded in the ``to`` or ``bcc`` attributes. This is
@@ -299,13 +314,13 @@ The class has the following methods:
Sending alternative content types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-It can be useful to include multiple versions of the content in an e-mail;
-the classic example is to send both text and HTML versions of a message. With
+It can be useful to include multiple versions of the content in an e-mail; the
+classic example is to send both text and HTML versions of a message. With
Django's e-mail library, you can do this using the ``EmailMultiAlternatives``
-class. This subclass of ``EmailMessage`` has an ``attach_alternative()`` method
-for including extra versions of the message body in the e-mail. All the other
-methods (including the class initialization) are inherited directly from
-``EmailMessage``.
+class. This subclass of :class:`~django.core.mail.EmailMessage` has an
+``attach_alternative()`` method for including extra versions of the message
+body in the e-mail. All the other methods (including the class initialization)
+are inherited directly from :class:`~django.core.mail.EmailMessage`.
To send a text and HTML combination, you could write::
@@ -318,41 +333,231 @@ To send a text and HTML combination, you could write::
msg.attach_alternative(html_content, "text/html")
msg.send()
-By default, the MIME type of the ``body`` parameter in an ``EmailMessage`` is
-``"text/plain"``. It is good practice to leave this alone, because it
-guarantees that any recipient will be able to read the e-mail, regardless of
-their mail client. However, if you are confident that your recipients can
-handle an alternative content type, you can use the ``content_subtype``
-attribute on the ``EmailMessage`` class to change the main content type. The
-major type will always be ``"text"``, but you can change it to the subtype. For
-example::
+By default, the MIME type of the ``body`` parameter in an
+:class:`~django.core.mail.EmailMessage` is ``"text/plain"``. It is good
+practice to leave this alone, because it guarantees that any recipient will be
+able to read the e-mail, regardless of their mail client. However, if you are
+confident that your recipients can handle an alternative content type, you can
+use the ``content_subtype`` attribute on the
+:class:`~django.core.mail.EmailMessage` class to change the main content type.
+The major type will always be ``"text"``, but you can change it to the
+subtype. For example::
msg = EmailMessage(subject, html_content, from_email, [to])
msg.content_subtype = "html" # Main content is now text/html
msg.send()
-SMTPConnection Objects
-----------------------
+.. _topic-email-backends:
-.. class:: SMTPConnection
+E-Mail Backends
+===============
+
+.. versionadded:: 1.2
+
+The actual sending of an e-mail is handled by the e-mail backend.
+
+The e-mail backend class has the following methods:
+
+ * ``open()`` instantiates an long-lived email-sending connection.
+
+ * ``close()`` closes the current email-sending connection.
+
+ * ``send_messages(email_messages)`` sends a list of
+ :class:`~django.core.mail.EmailMessage` objects. If the connection is
+ not open, this call will implicitly open the connection, and close the
+ connection afterwards. If the connection is already open, it will be
+ left open after mail has been sent.
+
+Obtaining an instance of an e-mail backend
+------------------------------------------
+
+The :meth:`get_connection` function in ``django.core.mail`` returns an
+instance of the e-mail backend that you can use.
+
+.. currentmodule:: django.core.mail
+
+.. function:: get_connection(backend=None, fail_silently=False, *args, **kwargs)
+
+By default, a call to ``get_connection()`` will return an instance of the
+email backend specified in :setting:`EMAIL_BACKEND`. If you specify the
+``backend`` argument, an instance of that backend will be instantiated.
+
+The ``fail_silently`` argument controls how the backend should handle errors.
+If ``fail_silently`` is True, exceptions during the email sending process
+will be silently ignored.
+
+All other arguments are passed directly to the constructor of the
+e-mail backend.
+
+Django ships with several e-mail sending backends. With the exception of the
+SMTP backend (which is the default), these backends are only useful during
+testing and development. If you have special email sending requirements, you
+can :ref:`write your own email backend <topic-custom-email-backend>`.
+
+.. _topic-email-smtp-backend:
+
+SMTP backend
+~~~~~~~~~~~~
+
+This is the default backend. E-mail will be sent through a SMTP server.
+The server address and authentication credentials are set in the
+:setting:`EMAIL_HOST`, :setting:`EMAIL_POST`, :setting:`EMAIL_HOST_USER`,
+:setting:`EMAIL_HOST_PASSWORD` and :setting:`EMAIL_USE_TLS` settings in your
+settings file.
+
+The SMTP backend is the default configuration inherited by Django. If you
+want to specify it explicitly, put the following in your settings::
+
+ EMAIL_BACKEND = 'django.core.mail.backends.smtp'
+
+.. admonition:: SMTPConnection objects
+
+ Prior to version 1.2, Django provided a
+ :class:`~django.core.mail.SMTPConnection` class. This class provided a way
+ to directly control the use of SMTP to send email. This class has been
+ deprecated in favor of the generic email backend API.
+
+ For backwards compatibility :class:`~django.core.mail.SMTPConnection` is
+ still available in ``django.core.mail`` as an alias for the SMTP backend.
+ New code should use :meth:`~django.core.mail.get_connection` instead.
+
+Console backend
+~~~~~~~~~~~~~~~
+
+Instead of sending out real e-mails the console backend just writes the
+e-mails that would be send to the standard output. By default, the console
+backend writes to ``stdout``. You can use a different stream-like object by
+providing the ``stream`` keyword argument when constructing the connection.
+
+To specify this backend, put the following in your settings::
+
+ EMAIL_BACKEND = 'django.core.mail.backends.console'
+
+This backend is not intended for use in production -- it is provided as a
+convenience that can be used during development.
+
+File backend
+~~~~~~~~~~~~
+
+The file backend writes e-mails to a file. A new file is created for each new
+session that is opened on this backend. The directory to which the files are
+written is either taken from the :setting:`EMAIL_FILE_PATH` setting or from
+the ``file_path`` keyword when creating a connection with
+:meth:`~django.core.mail.get_connection`.
+
+To specify this backend, put the following in your settings::
+
+ EMAIL_BACKEND = 'django.core.mail.backends.filebased'
+ EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location
+
+This backend is not intended for use in production -- it is provided as a
+convenience that can be used during development.
+
+In-memory backend
+~~~~~~~~~~~~~~~~~
-The ``SMTPConnection`` class is initialized with the host, port, username and
-password for the SMTP server. If you don't specify one or more of those
-options, they are read from your settings file.
+The ``'locmem'`` backend stores messages in a special attribute of the
+``django.core.mail`` module. The ``outbox`` attribute is created when the
+first message is send. It's a list with an
+:class:`~django.core.mail.EmailMessage` instance for each message that would
+be send.
-If you're sending lots of messages at once, the ``send_messages()`` method of
-the ``SMTPConnection`` class is useful. It takes a list of ``EmailMessage``
-instances (or subclasses) and sends them over a single connection. For example,
-if you have a function called ``get_notification_email()`` that returns a
-list of ``EmailMessage`` objects representing some periodic e-mail you wish to
-send out, you could send this with::
+To specify this backend, put the following in your settings::
- connection = SMTPConnection() # Use default settings for connection
+ EMAIL_BACKEND = 'django.core.mail.backends.locmem'
+
+This backend is not intended for use in production -- it is provided as a
+convenience that can be used during development and testing.
+
+Dummy backend
+~~~~~~~~~~~~~
+
+As the name suggests the dummy backend does nothing with your messages. To
+specify this backend, put the following in your settings::
+
+ EMAIL_BACKEND = 'django.core.mail.backends.dummy'
+
+This backend is not intended for use in production -- it is provided as a
+convenience that can be used during development.
+
+.. _topic-custom-email-backend:
+
+Defining a custom e-mail backend
+--------------------------------
+
+If you need to change how e-mails are send you can write your own e-mail
+backend. The ``EMAIL_BACKEND`` setting in your settings file is then the
+Python import path for your backend.
+
+Custom e-mail backends should subclass ``BaseEmailBackend`` that is located in
+the ``django.core.mail.backends.base`` module. A custom e-mail backend must
+implement the ``send_messages(email_messages)`` method. This method receives a
+list of :class:`~django.core.mail.EmailMessage` instances and returns the
+number of successfully delivered messages. If your backend has any concept of
+a persistent session or connection, you should also implement the ``open()``
+and ``close()`` methods. Refer to ``SMTPEmailBackend`` for a reference
+implementation.
+
+.. _topics-sending-multiple-emails:
+
+Sending multiple emails
+-----------------------
+
+Establishing and closing an SMTP connection (or any other network connection,
+for that matter) is an expensive process. If you have a lot of emails to send,
+it makes sense to reuse an SMTP connection, rather than creating and
+destroying a connection every time you want to send an email.
+
+There are two ways you tell an email backend to reuse a connection.
+
+Firstly, you can use the ``send_messages()`` method. ``send_messages()`` takes
+a list of :class:`~django.core.mail.EmailMessage` instances (or subclasses),
+and sends them all using a single connection.
+
+For example, if you have a function called ``get_notification_email()`` that
+returns a list of :class:`~django.core.mail.EmailMessage` objects representing
+some periodic e-mail you wish to send out, you could send these emails using
+a single call to send_messages::
+
+ from django.core import mail
+ connection = mail.get_connection() # Use default email connection
messages = get_notification_email()
connection.send_messages(messages)
+In this example, the call to ``send_messages()`` opens a connection on the
+backend, sends the list of messages, and then closes the connection again.
+
+The second approach is to use the ``open()`` and ``close()`` methods on the
+email backend to manually control the connection. ``send_messages()`` will not
+manually open or close the connection if it is already open, so if you
+manually open the connection, you can control when it is closed. For example::
+
+ from django.core import mail
+ connection = mail.get_connection()
+
+ # Manually open the connection
+ connection.open()
+
+ # Construct an email message that uses the connection
+ email1 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
+ ['to1@example.com'], connection=connection)
+ email1.send() # Send the email
+
+ # Construct two more messages
+ email2 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
+ ['to2@example.com'])
+ email3 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
+ ['to3@example.com'])
+
+ # Send the two emails in a single call -
+ connection.send_messages([email2, email3])
+ # The connection was already open so send_messages() doesn't close it.
+ # We need to manually close the connection.
+ connection.close()
+
+
Testing e-mail sending
-----------------------
+======================
The are times when you do not want Django to send e-mails at all. For example,
while developing a website, you probably don't want to send out thousands of
@@ -360,19 +565,41 @@ e-mails -- but you may want to validate that e-mails will be sent to the right
people under the right conditions, and that those e-mails will contain the
correct content.
-The easiest way to test your project's use of e-mail is to use a "dumb" e-mail
-server that receives the e-mails locally and displays them to the terminal,
-but does not actually send anything. Python has a built-in way to accomplish
-this with a single command::
+The easiest way to test your project's use of e-mail is to use the ``console``
+email backend. This backend redirects all email to stdout, allowing you to
+inspect the content of mail.
+
+The ``file`` email backend can also be useful during development -- this backend
+dumps the contents of every SMTP connection to a file that can be inspected
+at your leisure.
+
+Another approach is to use a "dumb" SMTP server that receives the e-mails
+locally and displays them to the terminal, but does not actually send
+anything. Python has a built-in way to accomplish this with a single command::
python -m smtpd -n -c DebuggingServer localhost:1025
This command will start a simple SMTP server listening on port 1025 of
-localhost. This server simply prints to standard output all email headers and
-the email body. You then only need to set the :setting:`EMAIL_HOST` and
+localhost. This server simply prints to standard output all e-mail headers and
+the e-mail body. You then only need to set the :setting:`EMAIL_HOST` and
:setting:`EMAIL_PORT` accordingly, and you are set.
-For more entailed testing and processing of e-mails locally, see the Python
-documentation on the `SMTP Server`_.
+For a more detailed discussion of testing and processing of e-mails locally,
+see the Python documentation on the `SMTP Server`_.
.. _SMTP Server: http://docs.python.org/library/smtpd.html
+
+SMTPConnection
+==============
+
+.. class:: SMTPConnection
+
+.. deprecated:: 1.2
+
+The ``SMTPConnection`` class has been deprecated in favor of the generic email
+backend API.
+
+For backwards compatibility ``SMTPConnection`` is still available in
+``django.core.mail`` as an alias for the :ref:`SMTP backend
+<topic-email-smtp-backend>`. New code should use
+:meth:`~django.core.mail.get_connection` instead.
diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt
index 19facb8371..c64ec7dc79 100644
--- a/docs/topics/http/middleware.txt
+++ b/docs/topics/http/middleware.txt
@@ -29,6 +29,7 @@ created by :djadmin:`django-admin.py startproject <startproject>`::
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)
diff --git a/docs/topics/install.txt b/docs/topics/install.txt
index b66c8aef15..0a64e3258d 100644
--- a/docs/topics/install.txt
+++ b/docs/topics/install.txt
@@ -11,7 +11,7 @@ Install Python
Being a Python Web framework, Django requires Python.
-It works with any Python version from 2.3 to 2.6 (due to backwards
+It works with any Python version from 2.4 to 2.6 (due to backwards
incompatibilities in Python 3.0, Django does not currently work with
Python 3.0; see :ref:`the Django FAQ <faq-install>` for more
information on supported Python versions and the 3.0 transition).
@@ -93,10 +93,10 @@ database bindings are installed.
will also want to read the database-specific notes for the :ref:`MySQL
backend <ref-databases>`.
-* If you're using SQLite and either Python 2.3 or Python 2.4, you'll need
- pysqlite_. Use version 2.0.3 or higher. Python 2.5 ships with an SQLite
- wrapper in the standard library, so you don't need to install anything extra
- in that case. Please read the SQLite backend :ref:`notes<sqlite-notes>`.
+* If you're using SQLite and Python 2.4, you'll need pysqlite_. Use version
+ 2.0.3 or higher. Python 2.5 ships with an SQLite wrapper in the standard
+ library, so you don't need to install anything extra in that case. Please
+ read the SQLite backend :ref:`notes<sqlite-notes>`.
* If you're using Oracle, you'll need a copy of cx_Oracle_, but please
read the database-specific notes for the
diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
index 25d2f083fd..6648461014 100644
--- a/docs/topics/testing.txt
+++ b/docs/topics/testing.txt
@@ -1104,6 +1104,8 @@ applications:
``target_status_code`` will be the url and status code for the final
point of the redirect chain.
+.. _topics-testing-email:
+
E-mail services
---------------
@@ -1117,7 +1119,7 @@ test every aspect of sending e-mail -- from the number of messages sent to the
contents of each message -- without actually sending the messages.
The test runner accomplishes this by transparently replacing the normal
-:class:`~django.core.mail.SMTPConnection` class with a different version.
+email backend with a testing backend.
(Don't worry -- this has no effect on any other e-mail senders outside of
Django, such as your machine's mail server, if you're running one.)
@@ -1128,14 +1130,8 @@ Django, such as your machine's mail server, if you're running one.)
During test running, each outgoing e-mail is saved in
``django.core.mail.outbox``. This is a simple list of all
:class:`~django.core.mail.EmailMessage` instances that have been sent.
-It does not exist under normal execution conditions, i.e., when you're not
-running unit tests. The outbox is created during test setup, along with the
-dummy :class:`~django.core.mail.SMTPConnection`. When the test framework is
-torn down, the standard :class:`~django.core.mail.SMTPConnection` class is
-restored, and the test outbox is destroyed.
-
The ``outbox`` attribute is a special attribute that is created *only* when
-the tests are run. It doesn't normally exist as part of the
+the ``locmem`` e-mail backend is used. It doesn't normally exist as part of the
:mod:`django.core.mail` module and you can't import it directly. The code
below shows how to access this attribute correctly.
diff --git a/extras/csrf_migration_helper.py b/extras/csrf_migration_helper.py
new file mode 100644
index 0000000000..bc352a1762
--- /dev/null
+++ b/extras/csrf_migration_helper.py
@@ -0,0 +1,369 @@
+#!/usr/bin/env python
+
+# This script aims to help developers locate forms and view code that needs to
+# use the new CSRF protection in Django 1.2. It tries to find all the code that
+# may need the steps described in the CSRF documentation. It does not modify
+# any code directly, it merely attempts to locate it. Developers should be
+# aware of its limitations, described below.
+#
+# For each template that contains at least one POST form, the following info is printed:
+#
+# <Absolute path to template>
+# AKA: <Aliases (relative to template directory/directories that contain it)>
+# POST forms: <Number of POST forms>
+# With token: <Number of POST forms with the CSRF token already added>
+# Without token:
+# <File name and line number of form without token>
+#
+# Searching for:
+# <Template names that need to be searched for in view code
+# (includes templates that 'include' current template)>
+#
+# Found:
+# <File name and line number of any view code found>
+#
+# The format used allows this script to be used in Emacs grep mode:
+# M-x grep
+# Run grep (like this): /path/to/my/virtualenv/python /path/to/django/src/extras/csrf_migration_helper.py --settings=mysettings /path/to/my/srcs
+
+
+# Limitations
+# ===========
+#
+# - All templates must be stored on disk in '.html' or '.htm' files.
+# (extensions configurable below)
+#
+# - All Python code must be stored on disk in '.py' files. (extensions
+# configurable below)
+#
+# - All templates must be accessible from TEMPLATE_DIRS or from the 'templates/'
+# directory in apps specified in INSTALLED_APPS. Non-file based template
+# loaders are out of the picture, because there is no way to ask them to
+# return all templates.
+#
+# - If you put the {% csrf_token %} tag on the same line as the <form> tag it
+# will be detected, otherwise it will be assumed that the form does not have
+# the token.
+#
+# - It's impossible to programmatically determine which forms should and should
+# not have the token added. The developer must decide when to do this,
+# ensuring that the token is only added to internally targetted forms.
+#
+# - It's impossible to programmatically work out when a template is used. The
+# attempts to trace back to view functions are guesses, and could easily fail
+# in the following ways:
+#
+# * If the 'include' template tag is used with a variable
+# i.e. {% include tname %} where tname is a variable containing the actual
+# template name, rather than {% include "my_template.html" %}.
+#
+# * If the template name has been built up by view code instead of as a simple
+# string. For example, generic views and the admin both do this. (These
+# apps are both contrib and both use RequestContext already, as it happens).
+#
+# * If the 'ssl' tag (or any template tag other than 'include') is used to
+# include the template in another template.
+#
+# - All templates belonging to apps referenced in INSTALLED_APPS will be
+# searched, which may include third party apps or Django contrib. In some
+# cases, this will be a good thing, because even if the templates of these
+# apps have been fixed by someone else, your own view code may reference the
+# same template and may need to be updated.
+#
+# You may, however, wish to comment out some entries in INSTALLED_APPS or
+# TEMPLATE_DIRS before running this script.
+
+# Improvements to this script are welcome!
+
+# Configuration
+# =============
+
+TEMPLATE_EXTENSIONS = [
+ ".html",
+ ".htm",
+ ]
+
+PYTHON_SOURCE_EXTENSIONS = [
+ ".py",
+ ]
+
+TEMPLATE_ENCODING = "UTF-8"
+
+PYTHON_ENCODING = "UTF-8"
+
+# Method
+# ======
+
+# Find templates:
+# - template dirs
+# - installed apps
+#
+# Search for POST forms
+# - Work out what the name of the template is, as it would appear in an
+# 'include' or get_template() call. This can be done by comparing template
+# filename to all template dirs. Some templates can have more than one
+# 'name' e.g. if a directory and one of its child directories are both in
+# TEMPLATE_DIRS. This is actually a common hack used for
+# overriding-and-extending admin templates.
+#
+# For each POST form,
+# - see if it already contains '{% csrf_token %}' immediately after <form>
+# - work back to the view function(s):
+# - First, see if the form is included in any other templates, then
+# recursively compile a list of affected templates.
+# - Find any code function that references that template. This is just a
+# brute force text search that can easily return false positives
+# and fail to find real instances.
+
+
+import os
+import sys
+import re
+try:
+ set
+except NameError:
+ from sets import Set as set
+
+
+USAGE = """
+This tool helps to locate forms that need CSRF tokens added and the
+corresponding view code. This processing is NOT fool proof, and you should read
+the help contained in the script itself. Also, this script may need configuring
+(by editing the script) before use.
+
+Usage:
+
+python csrf_migration_helper.py [--settings=path.to.your.settings] /path/to/python/code [more paths...]
+
+ Paths can be specified as relative paths.
+
+ With no arguments, this help is printed.
+"""
+
+_POST_FORM_RE = \
+ re.compile(r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE)
+_TOKEN_RE = re.compile('\{% csrf_token')
+
+def get_template_dirs():
+ """
+ Returns a set of all directories that contain project templates.
+ """
+ from django.conf import settings
+ dirs = set()
+ if 'django.template.loaders.filesystem.load_template_source' in settings.TEMPLATE_LOADERS:
+ dirs.update(map(unicode, settings.TEMPLATE_DIRS))
+
+ if 'django.template.loaders.app_directories.load_template_source' in settings.TEMPLATE_LOADERS:
+ from django.template.loaders.app_directories import app_template_dirs
+ dirs.update(app_template_dirs)
+ return dirs
+
+def make_template_info(filename, root_dirs):
+ """
+ Creates a Template object for a filename, calculating the possible
+ relative_filenames from the supplied filename and root template directories
+ """
+ return Template(filename,
+ [filename[len(d)+1:] for d in root_dirs if filename.startswith(d)])
+
+
+class Template(object):
+ def __init__(self, absolute_filename, relative_filenames):
+ self.absolute_filename, self.relative_filenames = absolute_filename, relative_filenames
+
+ def content(self):
+ try:
+ return self._content
+ except AttributeError:
+ fd = open(self.absolute_filename)
+ content = fd.read().decode(TEMPLATE_ENCODING)
+ fd.close()
+ self._content = content
+ return content
+ content = property(content)
+
+ def post_form_info(self):
+ """
+ Get information about any POST forms in the template.
+ Returns [(linenumber, csrf_token added)]
+ """
+ matches = []
+ for ln, line in enumerate(self.content.split("\n")):
+ m = _POST_FORM_RE.search(line)
+ if m is not None:
+ matches.append((ln + 1, _TOKEN_RE.search(line) is not None))
+ return matches
+
+ def includes_template(self, t):
+ """
+ Returns true if this template includes template 't' (via {% include %})
+ """
+ for r in t.relative_filenames:
+ if re.search(r'\{%\s*include\s+"' + re.escape(r) + r'"\s*%\}', self.content):
+ return True
+ return False
+
+ def related_templates(self):
+ """
+ Returns all templates that include this one, recursively. (starting
+ with this one)
+ """
+ try:
+ return self._related_templates
+ except AttributeError:
+ pass
+
+ retval = set([self])
+ for r in self.relative_filenames:
+ for t in self.all_templates:
+ if t.includes_template(self):
+ # If two templates mutually include each other, directly or
+ # indirectly, we have a problem here...
+ retval = retval.union(t.related_templates())
+
+ self._related_templates = retval
+ return retval
+
+ def __repr__(self):
+ return repr(self.absolute_filename)
+
+ def __eq__(self, other):
+ return self.absolute_filename == other.absolute_filename
+
+ def __hash__(self):
+ return hash(self.absolute_filename)
+
+def get_templates(dirs):
+ """
+ Returns all files in dirs that have template extensions, as Template
+ objects.
+ """
+ templates = set()
+ for root in dirs:
+ for (dirpath, dirnames, filenames) in os.walk(root):
+ for f in filenames:
+ if len([True for e in TEMPLATE_EXTENSIONS if f.endswith(e)]) > 0:
+ t = make_template_info(os.path.join(dirpath, f), dirs)
+ # templates need to be able to search others:
+ t.all_templates = templates
+ templates.add(t)
+ return templates
+
+def get_python_code(paths):
+ """
+ Returns all Python code, as a list of tuples, each one being:
+ (filename, list of lines)
+ """
+ retval = []
+ for p in paths:
+ for (dirpath, dirnames, filenames) in os.walk(p):
+ for f in filenames:
+ if len([True for e in PYTHON_SOURCE_EXTENSIONS if f.endswith(e)]) > 0:
+ fn = os.path.join(dirpath, f)
+ fd = open(fn)
+ content = [l.decode(PYTHON_ENCODING) for l in fd.readlines()]
+ fd.close()
+ retval.append((fn, content))
+ return retval
+
+def search_python_list(python_code, template_names):
+ """
+ Searches python code for a list of template names.
+ Returns a list of tuples, each one being:
+ (filename, line number)
+ """
+ retval = []
+ for tn in template_names:
+ retval.extend(search_python(python_code, tn))
+ retval = list(set(retval))
+ retval.sort()
+ return retval
+
+def search_python(python_code, template_name):
+ """
+ Searches Python code for a template name.
+ Returns a list of tuples, each one being:
+ (filename, line number)
+ """
+ retval = []
+ for fn, content in python_code:
+ for ln, line in enumerate(content):
+ if ((u'"%s"' % template_name) in line) or \
+ ((u"'%s'" % template_name) in line):
+ retval.append((fn, ln + 1))
+ return retval
+
+def main(pythonpaths):
+ template_dirs = get_template_dirs()
+ templates = get_templates(template_dirs)
+ python_code = get_python_code(pythonpaths)
+ for t in templates:
+ # Logic
+ form_matches = t.post_form_info()
+ num_post_forms = len(form_matches)
+ form_lines_without_token = [ln for (ln, has_token) in form_matches if not has_token]
+ if num_post_forms == 0:
+ continue
+ to_search = [rf for rt in t.related_templates() for rf in rt.relative_filenames]
+ found = search_python_list(python_code, to_search)
+
+ # Display:
+ print t.absolute_filename
+ for r in t.relative_filenames:
+ print u" AKA %s" % r
+ print u" POST forms: %s" % num_post_forms
+ print u" With token: %s" % (num_post_forms - len(form_lines_without_token))
+ if form_lines_without_token:
+ print u" Without token:"
+ for ln in form_lines_without_token:
+ print "%s:%d:" % (t.absolute_filename, ln)
+ print
+ print u" Searching for:"
+ for r in to_search:
+ print u" " + r
+ print
+ print u" Found:"
+ if len(found) == 0:
+ print " Nothing"
+ else:
+ for fn, ln in found:
+ print "%s:%d:" % (fn, ln)
+
+ print
+ print "----"
+
+
+if __name__ == '__main__':
+ # Hacky argument parsing, one day I'll learn OptParse...
+ args = list(sys.argv[1:])
+ if len(args) > 0:
+ if args[0] in ['--help', '-h', '-?', '--usage']:
+ print USAGE
+ sys.exit(0)
+ else:
+ if args[0].startswith('--settings='):
+ module = args[0][len('--settings='):]
+ os.environ["DJANGO_SETTINGS_MODULE"] = module
+ args = args[1:]
+
+ if args[0].startswith('-'):
+ print "Unknown option: %s" % args[0]
+ print USAGE
+ sys.exit(1)
+
+ pythonpaths = args
+
+ if os.environ.get("DJANGO_SETTINGS_MODULE", None) is None:
+ print "You need to set DJANGO_SETTINGS_MODULE or use the '--settings' parameter"
+ sys.exit(1)
+ if len(pythonpaths) == 0:
+ print "Unrecognised command: %s" % command
+ print USAGE
+ sys.exit(1)
+
+ main(pythonpaths)
+
+ else:
+ # no args
+ print USAGE
+ sys.exit(0)
diff --git a/tests/modeltests/invalid_models/models.py b/tests/modeltests/invalid_models/models.py
index c033d31237..af199635e6 100644
--- a/tests/modeltests/invalid_models/models.py
+++ b/tests/modeltests/invalid_models/models.py
@@ -182,6 +182,7 @@ class UniqueM2M(models.Model):
""" Model to test for unique ManyToManyFields, which are invalid. """
unique_people = models.ManyToManyField( Person, unique=True )
+
model_errors = """invalid_models.fielderrors: "charfield": CharFields require a "max_length" attribute.
invalid_models.fielderrors: "decimalfield": DecimalFields require a "decimal_places" attribute.
invalid_models.fielderrors: "decimalfield": DecimalFields require a "max_digits" attribute.
diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py
index 11e2b079f0..94c16ff071 100644
--- a/tests/modeltests/lookup/models.py
+++ b/tests/modeltests/lookup/models.py
@@ -17,6 +17,10 @@ class Article(models.Model):
return self.headline
__test__ = {'API_TESTS': r"""
+# We can use .exists() to check that there are none yet
+>>> Article.objects.exists()
+False
+
# Create a couple of Articles.
>>> from datetime import datetime
>>> a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26))
@@ -33,6 +37,10 @@ __test__ = {'API_TESTS': r"""
>>> a6.save()
>>> a7 = Article(headline='Article 7', pub_date=datetime(2005, 7, 27))
>>> a7.save()
+
+# There should be some now!
+>>> Article.objects.exists()
+True
"""}
if settings.DATABASE_ENGINE in ('postgresql', 'postgresql_pysycopg2'):
diff --git a/tests/modeltests/m2m_through/models.py b/tests/modeltests/m2m_through/models.py
index 10aa163343..16f303d02e 100644
--- a/tests/modeltests/m2m_through/models.py
+++ b/tests/modeltests/m2m_through/models.py
@@ -133,7 +133,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'add'
>>> rock.members.create(name='Anne')
Traceback (most recent call last):
...
-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
# Remove has similar complications, and is not provided either.
>>> rock.members.remove(jim)
@@ -160,7 +160,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
>>> rock.members = backup
Traceback (most recent call last):
...
-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
# Let's re-save those instances that we've cleared.
>>> m1.save()
@@ -184,7 +184,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'add'
>>> bob.group_set.create(name='Funk')
Traceback (most recent call last):
...
-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
# Remove has similar complications, and is not provided either.
>>> jim.group_set.remove(rock)
@@ -209,7 +209,7 @@ AttributeError: 'ManyRelatedManager' object has no attribute 'remove'
>>> jim.group_set = backup
Traceback (most recent call last):
...
-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through.Membership's Manager instead.
# Let's re-save those instances that we've cleared.
>>> m1.save()
@@ -334,4 +334,4 @@ AttributeError: Cannot set values on a ManyToManyField which specifies an interm
# QuerySet's distinct() method can correct this problem.
>>> Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct()
[<Person: Jane>, <Person: Jim>]
-"""} \ No newline at end of file
+"""}
diff --git a/tests/modeltests/model_package/__init__.py b/tests/modeltests/model_package/__init__.py
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/tests/modeltests/model_package/__init__.py
@@ -0,0 +1 @@
+
diff --git a/tests/modeltests/model_package/models/__init__.py b/tests/modeltests/model_package/models/__init__.py
new file mode 100644
index 0000000000..91e1b02e7d
--- /dev/null
+++ b/tests/modeltests/model_package/models/__init__.py
@@ -0,0 +1,3 @@
+# Import all the models from subpackages
+from article import Article
+from publication import Publication
diff --git a/tests/modeltests/model_package/models/article.py b/tests/modeltests/model_package/models/article.py
new file mode 100644
index 0000000000..c8fae1c72d
--- /dev/null
+++ b/tests/modeltests/model_package/models/article.py
@@ -0,0 +1,10 @@
+from django.db import models
+from django.contrib.sites.models import Site
+
+class Article(models.Model):
+ sites = models.ManyToManyField(Site)
+ headline = models.CharField(max_length=100)
+ publications = models.ManyToManyField("model_package.Publication", null=True, blank=True,)
+
+ class Meta:
+ app_label = 'model_package'
diff --git a/tests/modeltests/model_package/models/publication.py b/tests/modeltests/model_package/models/publication.py
new file mode 100644
index 0000000000..4dc2d6a148
--- /dev/null
+++ b/tests/modeltests/model_package/models/publication.py
@@ -0,0 +1,7 @@
+from django.db import models
+
+class Publication(models.Model):
+ title = models.CharField(max_length=30)
+
+ class Meta:
+ app_label = 'model_package'
diff --git a/tests/modeltests/model_package/tests.py b/tests/modeltests/model_package/tests.py
new file mode 100644
index 0000000000..6e8c158a68
--- /dev/null
+++ b/tests/modeltests/model_package/tests.py
@@ -0,0 +1,34 @@
+"""
+>>> from models.publication import Publication
+>>> from models.article import Article
+>>> from django.contrib.auth.views import Site
+
+>>> p = Publication(title="FooBar")
+>>> p.save()
+>>> p
+<Publication: Publication object>
+
+>>> from django.contrib.sites.models import Site
+>>> current_site = Site.objects.get_current()
+>>> current_site
+<Site: example.com>
+
+# Regression for #12168: models split into subpackages still get M2M tables
+
+>>> a = Article(headline="a foo headline")
+>>> a.save()
+>>> a.publications.add(p)
+>>> a.sites.add(current_site)
+>>> a.save()
+
+>>> a = Article.objects.get(id=1)
+>>> a
+<Article: Article object>
+>>> a.id
+1
+>>> a.sites.count()
+1
+
+"""
+
+
diff --git a/tests/regressiontests/admin_validation/models.py b/tests/regressiontests/admin_validation/models.py
index 1ff89e2502..5506114841 100644
--- a/tests/regressiontests/admin_validation/models.py
+++ b/tests/regressiontests/admin_validation/models.py
@@ -4,9 +4,11 @@ Tests of ModelAdmin validation logic.
from django.db import models
+
class Album(models.Model):
title = models.CharField(max_length=150)
+
class Song(models.Model):
title = models.CharField(max_length=150)
album = models.ForeignKey(Album)
@@ -17,11 +19,19 @@ class Song(models.Model):
def __unicode__(self):
return self.title
+
+class TwoAlbumFKAndAnE(models.Model):
+ album1 = models.ForeignKey(Album, related_name="album1_set")
+ album2 = models.ForeignKey(Album, related_name="album2_set")
+ e = models.CharField(max_length=1)
+
+
+
__test__ = {'API_TESTS':"""
>>> from django import forms
>>> from django.contrib import admin
->>> from django.contrib.admin.validation import validate
+>>> from django.contrib.admin.validation import validate, validate_inline
# Regression test for #8027: custom ModelForms with fields/fieldsets
@@ -58,4 +68,31 @@ Traceback (most recent call last):
...
ImproperlyConfigured: SongInline cannot exclude the field 'album' - this is the foreign key to the parent model Album.
+# Regression test for #11709 - when testing for fk excluding (when exclude is
+# given) make sure fk_name is honored or things blow up when there is more
+# than one fk to the parent model.
+
+>>> class TwoAlbumFKAndAnEInline(admin.TabularInline):
+... model = TwoAlbumFKAndAnE
+... exclude = ("e",)
+... fk_name = "album1"
+
+>>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
+
+# Ensure inlines validate that they can be used correctly.
+
+>>> class TwoAlbumFKAndAnEInline(admin.TabularInline):
+... model = TwoAlbumFKAndAnE
+
+>>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
+Traceback (most recent call last):
+ ...
+Exception: <class 'regressiontests.admin_validation.models.TwoAlbumFKAndAnE'> has more than 1 ForeignKey to <class 'regressiontests.admin_validation.models.Album'>
+
+>>> class TwoAlbumFKAndAnEInline(admin.TabularInline):
+... model = TwoAlbumFKAndAnE
+... fk_name = "album1"
+
+>>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
+
"""}
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
index 7273d3f320..8607589289 100644
--- a/tests/regressiontests/admin_views/tests.py
+++ b/tests/regressiontests/admin_views/tests.py
@@ -885,8 +885,9 @@ class AdminViewListEditable(TestCase):
# 4 action inputs (3 regular checkboxes, 1 checkbox to select all)
# main form submit button = 1
# search field and search submit button = 2
- # 6 + 2 + 1 + 2 = 11 inputs
- self.failUnlessEqual(response.content.count("<input"), 15)
+ # CSRF field = 1
+ # 6 + 2 + 4 + 1 + 2 + 1 = 16 inputs
+ self.failUnlessEqual(response.content.count("<input"), 16)
# 1 select per object = 3 selects
self.failUnlessEqual(response.content.count("<select"), 4)
@@ -1140,6 +1141,16 @@ class AdminActionsTest(TestCase):
'<input type="checkbox" class="action-select"' not in response.content,
"Found an unexpected action toggle checkboxbox in response"
)
+ self.assert_('action-checkbox-column' not in response.content,
+ "Found unexpected action-checkbox-column class in response")
+
+ def test_action_column_class(self):
+ "Tests that the checkbox column class is present in the response"
+ response = self.client.get('/test_admin/admin/admin_views/subscriber/')
+ self.assertNotEquals(response.context["action_form"], None)
+ self.assert_('action-checkbox-column' in response.content,
+ "Expected an action-checkbox-column in response")
+
def test_multiple_actions_form(self):
"""
diff --git a/tests/regressiontests/cache/models.py b/tests/regressiontests/cache/models.py
index e69de29bb2..643fd22629 100644
--- a/tests/regressiontests/cache/models.py
+++ b/tests/regressiontests/cache/models.py
@@ -0,0 +1,11 @@
+from django.db import models
+from datetime import datetime
+
+def expensive_calculation():
+ expensive_calculation.num_runs += 1
+ return datetime.now()
+
+class Poll(models.Model):
+ question = models.CharField(max_length=200)
+ answer = models.CharField(max_length=200)
+ pub_date = models.DateTimeField('date published', default=expensive_calculation)
diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
index 1b54ab935f..593b7262aa 100644
--- a/tests/regressiontests/cache/tests.py
+++ b/tests/regressiontests/cache/tests.py
@@ -16,6 +16,7 @@ from django.core.cache.backends.base import InvalidCacheBackendError
from django.http import HttpResponse, HttpRequest
from django.utils.cache import patch_vary_headers, get_cache_key, learn_cache_key
from django.utils.hashcompat import md5_constructor
+from regressiontests.cache.models import Poll, expensive_calculation
# functions/classes for complex data type tests
def f():
@@ -211,6 +212,47 @@ class BaseCacheTests(object):
self.cache.set("stuff", stuff)
self.assertEqual(self.cache.get("stuff"), stuff)
+ def test_cache_read_for_model_instance(self):
+ # Don't want fields with callable as default to be called on cache read
+ expensive_calculation.num_runs = 0
+ Poll.objects.all().delete()
+ my_poll = Poll.objects.create(question="Well?")
+ self.assertEqual(Poll.objects.count(), 1)
+ pub_date = my_poll.pub_date
+ self.cache.set('question', my_poll)
+ cached_poll = self.cache.get('question')
+ self.assertEqual(cached_poll.pub_date, pub_date)
+ # We only want the default expensive calculation run once
+ self.assertEqual(expensive_calculation.num_runs, 1)
+
+ def test_cache_write_for_model_instance_with_deferred(self):
+ # Don't want fields with callable as default to be called on cache write
+ expensive_calculation.num_runs = 0
+ Poll.objects.all().delete()
+ my_poll = Poll.objects.create(question="What?")
+ self.assertEqual(expensive_calculation.num_runs, 1)
+ defer_qs = Poll.objects.all().defer('question')
+ self.assertEqual(defer_qs.count(), 1)
+ self.assertEqual(expensive_calculation.num_runs, 1)
+ self.cache.set('deferred_queryset', defer_qs)
+ # cache set should not re-evaluate default functions
+ self.assertEqual(expensive_calculation.num_runs, 1)
+
+ def test_cache_read_for_model_instance_with_deferred(self):
+ # Don't want fields with callable as default to be called on cache read
+ expensive_calculation.num_runs = 0
+ Poll.objects.all().delete()
+ my_poll = Poll.objects.create(question="What?")
+ self.assertEqual(expensive_calculation.num_runs, 1)
+ defer_qs = Poll.objects.all().defer('question')
+ self.assertEqual(defer_qs.count(), 1)
+ self.cache.set('deferred_queryset', defer_qs)
+ self.assertEqual(expensive_calculation.num_runs, 1)
+ runs_before_cache_read = expensive_calculation.num_runs
+ cached_polls = self.cache.get('deferred_queryset')
+ # We only want the default expensive calculation run on creation and set
+ self.assertEqual(expensive_calculation.num_runs, runs_before_cache_read)
+
def test_expiration(self):
# Cache values can be set to expire
self.cache.set('expire1', 'very quickly', 1)
diff --git a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py
index b9eadd78b4..c9b50e2983 100644
--- a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py
+++ b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py
@@ -159,31 +159,32 @@ class ApproveViewTests(CommentTestCase):
response = self.client.get("/approved/", data={"c":pk})
self.assertTemplateUsed(response, "comments/approved.html")
+class AdminActionsTests(CommentTestCase):
+ urls = "regressiontests.comment_tests.urls_admin"
+
+ def setUp(self):
+ super(AdminActionsTests, self).setUp()
+
+ # Make "normaluser" a moderator
+ u = User.objects.get(username="normaluser")
+ u.is_staff = True
+ perms = Permission.objects.filter(
+ content_type__app_label = 'comments',
+ codename__endswith = 'comment'
+ )
+ for perm in perms:
+ u.user_permissions.add(perm)
+ u.save()
-class ModerationQueueTests(CommentTestCase):
-
- def testModerationQueuePermissions(self):
- """Only moderators can view the moderation queue"""
+ def testActionsNonModerator(self):
+ comments = self.createSomeComments()
self.client.login(username="normaluser", password="normaluser")
- response = self.client.get("/moderate/")
- self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/moderate/")
-
- makeModerator("normaluser")
- response = self.client.get("/moderate/")
- self.assertEqual(response.status_code, 200)
+ response = self.client.get("/admin/comments/comment/")
+ self.assertEquals("approve_comments" in response.content, False)
- def testModerationQueueContents(self):
- """Moderation queue should display non-public, non-removed comments."""
- c1, c2, c3, c4 = self.createSomeComments()
+ def testActionsModerator(self):
+ comments = self.createSomeComments()
makeModerator("normaluser")
self.client.login(username="normaluser", password="normaluser")
-
- c1.is_public = c2.is_public = False
- c1.save(); c2.save()
- response = self.client.get("/moderate/")
- self.assertEqual(list(response.context[0]["comments"]), [c1, c2])
-
- c2.is_removed = True
- c2.save()
- response = self.client.get("/moderate/")
- self.assertEqual(list(response.context[0]["comments"]), [c1])
+ response = self.client.get("/admin/comments/comment/")
+ self.assertEquals("approve_comments" in response.content, True)
diff --git a/tests/regressiontests/comment_tests/urls_admin.py b/tests/regressiontests/comment_tests/urls_admin.py
new file mode 100644
index 0000000000..341285d7ef
--- /dev/null
+++ b/tests/regressiontests/comment_tests/urls_admin.py
@@ -0,0 +1,13 @@
+from django.conf.urls.defaults import *
+from django.contrib import admin
+from django.contrib.comments.admin import CommentsAdmin
+from django.contrib.comments.models import Comment
+
+# Make a new AdminSite to avoid picking up the deliberately broken admin
+# modules in other tests.
+admin_site = admin.AdminSite()
+admin_site.register(Comment, CommentsAdmin)
+
+urlpatterns = patterns('',
+ (r'^admin/', include(admin_site.urls)),
+)
diff --git a/tests/regressiontests/context_processors/fixtures/context-processors-users.xml b/tests/regressiontests/context_processors/fixtures/context-processors-users.xml
new file mode 100644
index 0000000000..aba8f4aace
--- /dev/null
+++ b/tests/regressiontests/context_processors/fixtures/context-processors-users.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+ <object pk="100" model="auth.user">
+ <field type="CharField" name="username">super</field>
+ <field type="CharField" name="first_name">Super</field>
+ <field type="CharField" name="last_name">User</field>
+ <field type="CharField" name="email">super@example.com</field>
+ <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
+ <field type="BooleanField" name="is_staff">True</field>
+ <field type="BooleanField" name="is_active">True</field>
+ <field type="BooleanField" name="is_superuser">True</field>
+ <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
+ <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
+ <field to="auth.group" name="groups" rel="ManyToManyRel"></field>
+ <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
+ </object>
+</django-objects>
diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html
new file mode 100644
index 0000000000..b5c65db28d
--- /dev/null
+++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_access.html
@@ -0,0 +1 @@
+{{ user }}
diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html
new file mode 100644
index 0000000000..7b7e448ad2
--- /dev/null
+++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_messages.html
@@ -0,0 +1 @@
+{% for m in messages %}{{ m }}{% endfor %}
diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html
new file mode 100644
index 0000000000..8d1c8b69c3
--- /dev/null
+++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_no_access.html
@@ -0,0 +1 @@
+
diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html
new file mode 100644
index 0000000000..a5db868e9e
--- /dev/null
+++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_perms.html
@@ -0,0 +1 @@
+{% if perms.auth %}Has auth permissions{% endif %}
diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html
new file mode 100644
index 0000000000..a28ff937f8
--- /dev/null
+++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_test_access.html
@@ -0,0 +1 @@
+{% if session_accessed %}Session accessed{% else %}Session not accessed{% endif %}
diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html
new file mode 100644
index 0000000000..7ed16d7867
--- /dev/null
+++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html
@@ -0,0 +1,4 @@
+unicode: {{ user }}
+id: {{ user.id }}
+username: {{ user.username }}
+url: {% url userpage user %}
diff --git a/tests/regressiontests/context_processors/tests.py b/tests/regressiontests/context_processors/tests.py
index eadd6310b1..004f90eceb 100644
--- a/tests/regressiontests/context_processors/tests.py
+++ b/tests/regressiontests/context_processors/tests.py
@@ -3,8 +3,10 @@ Tests for Django's bundled context processors.
"""
from django.conf import settings
+from django.contrib.auth import authenticate
+from django.db.models import Q
from django.test import TestCase
-
+from django.template import Template
class RequestContextProcessorTests(TestCase):
"""
@@ -36,3 +38,75 @@ class RequestContextProcessorTests(TestCase):
self.assertContains(response, url)
response = self.client.post(url, {'path': '/blah/'})
self.assertContains(response, url)
+
+class AuthContextProcessorTests(TestCase):
+ """
+ Tests for the ``django.core.context_processors.auth`` processor
+ """
+ urls = 'regressiontests.context_processors.urls'
+ fixtures = ['context-processors-users.xml']
+
+ def test_session_not_accessed(self):
+ """
+ Tests that the session is not accessed simply by including
+ the auth context processor
+ """
+ response = self.client.get('/auth_processor_no_attr_access/')
+ self.assertContains(response, "Session not accessed")
+
+ def test_session_is_accessed(self):
+ """
+ Tests that the session is accessed if the auth context processor
+ is used and relevant attributes accessed.
+ """
+ response = self.client.get('/auth_processor_attr_access/')
+ self.assertContains(response, "Session accessed")
+
+ def test_perms_attrs(self):
+ self.client.login(username='super', password='secret')
+ response = self.client.get('/auth_processor_perms/')
+ self.assertContains(response, "Has auth permissions")
+
+ def test_message_attrs(self):
+ self.client.login(username='super', password='secret')
+ response = self.client.get('/auth_processor_messages/')
+ self.assertContains(response, "Message 1")
+
+ def test_user_attrs(self):
+ """
+ Test that the lazy objects returned behave just like the wrapped objects.
+ """
+ # These are 'functional' level tests for common use cases. Direct
+ # testing of the implementation (SimpleLazyObject) is in the 'utils'
+ # tests.
+ self.client.login(username='super', password='secret')
+ user = authenticate(username='super', password='secret')
+ response = self.client.get('/auth_processor_user/')
+ self.assertContains(response, "unicode: super")
+ self.assertContains(response, "id: 100")
+ self.assertContains(response, "username: super")
+ # bug #12037 is tested by the {% url %} in the template:
+ self.assertContains(response, "url: /userpage/super/")
+
+ # See if this object can be used for queries where a Q() comparing
+ # a user can be used with another Q() (in an AND or OR fashion).
+ # This simulates what a template tag might do with the user from the
+ # context. Note that we don't need to execute a query, just build it.
+ #
+ # The failure case (bug #12049) on Python 2.4 with a LazyObject-wrapped
+ # User is a fatal TypeError: "function() takes at least 2 arguments
+ # (0 given)" deep inside deepcopy().
+ #
+ # Python 2.5 and 2.6 succeeded, but logged internally caught exception
+ # spew:
+ #
+ # Exception RuntimeError: 'maximum recursion depth exceeded while
+ # calling a Python object' in <type 'exceptions.AttributeError'>
+ # ignored"
+ query = Q(user=response.context['user']) & Q(someflag=True)
+
+ # Tests for user equality. This is hard because User defines
+ # equality in a non-duck-typing way
+ # See bug #12060
+ self.assertEqual(response.context['user'], user)
+ self.assertEqual(user, response.context['user'])
diff --git a/tests/regressiontests/context_processors/urls.py b/tests/regressiontests/context_processors/urls.py
index 7e8ba967c1..30728c8df8 100644
--- a/tests/regressiontests/context_processors/urls.py
+++ b/tests/regressiontests/context_processors/urls.py
@@ -5,4 +5,10 @@ import views
urlpatterns = patterns('',
(r'^request_attrs/$', views.request_processor),
+ (r'^auth_processor_no_attr_access/$', views.auth_processor_no_attr_access),
+ (r'^auth_processor_attr_access/$', views.auth_processor_attr_access),
+ (r'^auth_processor_user/$', views.auth_processor_user),
+ (r'^auth_processor_perms/$', views.auth_processor_perms),
+ (r'^auth_processor_messages/$', views.auth_processor_messages),
+ url(r'^userpage/(.+)/$', views.userpage, name="userpage"),
)
diff --git a/tests/regressiontests/context_processors/views.py b/tests/regressiontests/context_processors/views.py
index 66e7132c05..3f2dcb037b 100644
--- a/tests/regressiontests/context_processors/views.py
+++ b/tests/regressiontests/context_processors/views.py
@@ -6,3 +6,32 @@ from django.template.context import RequestContext
def request_processor(request):
return render_to_response('context_processors/request_attrs.html',
RequestContext(request, {}, processors=[context_processors.request]))
+
+def auth_processor_no_attr_access(request):
+ r1 = render_to_response('context_processors/auth_attrs_no_access.html',
+ RequestContext(request, {}, processors=[context_processors.auth]))
+ # *After* rendering, we check whether the session was accessed
+ return render_to_response('context_processors/auth_attrs_test_access.html',
+ {'session_accessed':request.session.accessed})
+
+def auth_processor_attr_access(request):
+ r1 = render_to_response('context_processors/auth_attrs_access.html',
+ RequestContext(request, {}, processors=[context_processors.auth]))
+ return render_to_response('context_processors/auth_attrs_test_access.html',
+ {'session_accessed':request.session.accessed})
+
+def auth_processor_user(request):
+ return render_to_response('context_processors/auth_attrs_user.html',
+ RequestContext(request, {}, processors=[context_processors.auth]))
+
+def auth_processor_perms(request):
+ return render_to_response('context_processors/auth_attrs_perms.html',
+ RequestContext(request, {}, processors=[context_processors.auth]))
+
+def auth_processor_messages(request):
+ request.user.message_set.create(message="Message 1")
+ return render_to_response('context_processors/auth_attrs_messages.html',
+ RequestContext(request, {}, processors=[context_processors.auth]))
+
+def userpage(request):
+ pass
diff --git a/tests/regressiontests/csrf_tests/__init__.py b/tests/regressiontests/csrf_tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/csrf_tests/__init__.py
diff --git a/django/contrib/csrf/models.py b/tests/regressiontests/csrf_tests/models.py
index 71abcc5198..71abcc5198 100644
--- a/django/contrib/csrf/models.py
+++ b/tests/regressiontests/csrf_tests/models.py
diff --git a/tests/regressiontests/csrf_tests/tests.py b/tests/regressiontests/csrf_tests/tests.py
new file mode 100644
index 0000000000..5688293647
--- /dev/null
+++ b/tests/regressiontests/csrf_tests/tests.py
@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+
+from django.test import TestCase
+from django.http import HttpRequest, HttpResponse
+from django.middleware.csrf import CsrfMiddleware, CsrfViewMiddleware
+from django.views.decorators.csrf import csrf_exempt
+from django.core.context_processors import csrf
+from django.contrib.sessions.middleware import SessionMiddleware
+from django.utils.importlib import import_module
+from django.conf import settings
+from django.template import RequestContext, Template
+
+# Response/views used for CsrfResponseMiddleware and CsrfViewMiddleware tests
+def post_form_response():
+ resp = HttpResponse(content="""
+<html><body><form method="POST"><input type="text" /></form></body></html>
+""", mimetype="text/html")
+ return resp
+
+def post_form_response_non_html():
+ resp = post_form_response()
+ resp["Content-Type"] = "application/xml"
+ return resp
+
+def post_form_view(request):
+ """A view that returns a POST form (without a token)"""
+ return post_form_response()
+
+# Response/views used for template tag tests
+def _token_template():
+ return Template("{% csrf_token %}")
+
+def _render_csrf_token_template(req):
+ context = RequestContext(req, processors=[csrf])
+ template = _token_template()
+ return template.render(context)
+
+def token_view(request):
+ """A view that uses {% csrf_token %}"""
+ return HttpResponse(_render_csrf_token_template(request))
+
+def non_token_view_using_request_processor(request):
+ """
+ A view that doesn't use the token, but does use the csrf view processor.
+ """
+ context = RequestContext(request, processors=[csrf])
+ template = Template("")
+ return HttpResponse(template.render(context))
+
+class TestingHttpRequest(HttpRequest):
+ """
+ A version of HttpRequest that allows us to change some things
+ more easily
+ """
+ def is_secure(self):
+ return getattr(self, '_is_secure', False)
+
+class CsrfMiddlewareTest(TestCase):
+ _csrf_id = "1"
+
+ # This is a valid session token for this ID and secret key. This was generated using
+ # the old code that we're to be backwards-compatible with. Don't use the CSRF code
+ # to generate this hash, or we're merely testing the code against itself and not
+ # checking backwards-compatibility. This is also the output of (echo -n test1 | md5sum).
+ _session_token = "5a105e8b9d40e1329780d62ea2265d8a"
+ _session_id = "1"
+ _secret_key_for_session_test= "test"
+
+ def _get_GET_no_csrf_cookie_request(self):
+ return TestingHttpRequest()
+
+ def _get_GET_csrf_cookie_request(self):
+ req = TestingHttpRequest()
+ req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id
+ return req
+
+ def _get_POST_csrf_cookie_request(self):
+ req = self._get_GET_csrf_cookie_request()
+ req.method = "POST"
+ return req
+
+ def _get_POST_no_csrf_cookie_request(self):
+ req = self._get_GET_no_csrf_cookie_request()
+ req.method = "POST"
+ return req
+
+ def _get_POST_request_with_token(self):
+ req = self._get_POST_csrf_cookie_request()
+ req.POST['csrfmiddlewaretoken'] = self._csrf_id
+ return req
+
+ def _get_POST_session_request_with_token(self):
+ req = self._get_POST_no_csrf_cookie_request()
+ req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id
+ req.POST['csrfmiddlewaretoken'] = self._session_token
+ return req
+
+ def _get_POST_session_request_no_token(self):
+ req = self._get_POST_no_csrf_cookie_request()
+ req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id
+ return req
+
+ def _check_token_present(self, response, csrf_id=None):
+ self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id))
+
+ # Check the post processing and outgoing cookie
+ def test_process_response_no_csrf_cookie(self):
+ """
+ When no prior CSRF cookie exists, check that the cookie is created and a
+ token is inserted.
+ """
+ req = self._get_GET_no_csrf_cookie_request()
+ CsrfMiddleware().process_view(req, post_form_view, (), {})
+
+ resp = post_form_response()
+ resp_content = resp.content # needed because process_response modifies resp
+ resp2 = CsrfMiddleware().process_response(req, resp)
+
+ csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
+ self.assertNotEqual(csrf_cookie, False)
+ self.assertNotEqual(resp_content, resp2.content)
+ self._check_token_present(resp2, csrf_cookie.value)
+ # Check the Vary header got patched correctly
+ self.assert_('Cookie' in resp2.get('Vary',''))
+
+ def test_process_response_no_csrf_cookie_view_only_get_token_used(self):
+ """
+ When no prior CSRF cookie exists, check that the cookie is created, even
+ if only CsrfViewMiddleware is used.
+ """
+ # This is checking that CsrfViewMiddleware has the cookie setting
+ # code. Most of the other tests use CsrfMiddleware.
+ req = self._get_GET_no_csrf_cookie_request()
+ # token_view calls get_token() indirectly
+ CsrfViewMiddleware().process_view(req, token_view, (), {})
+ resp = token_view(req)
+ resp2 = CsrfViewMiddleware().process_response(req, resp)
+
+ csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
+ self.assertNotEqual(csrf_cookie, False)
+
+ def test_process_response_get_token_not_used(self):
+ """
+ Check that if get_token() is not called, the view middleware does not
+ add a cookie.
+ """
+ # This is important to make pages cacheable. Pages which do call
+ # get_token(), assuming they use the token, are not cacheable because
+ # the token is specific to the user
+ req = self._get_GET_no_csrf_cookie_request()
+ # non_token_view_using_request_processor does not call get_token(), but
+ # does use the csrf request processor. By using this, we are testing
+ # that the view processor is properly lazy and doesn't call get_token()
+ # until needed.
+ CsrfViewMiddleware().process_view(req, non_token_view_using_request_processor, (), {})
+ resp = non_token_view_using_request_processor(req)
+ resp2 = CsrfViewMiddleware().process_response(req, resp)
+
+ csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
+ self.assertEqual(csrf_cookie, False)
+
+ def test_process_response_existing_csrf_cookie(self):
+ """
+ Check that the token is inserted when a prior CSRF cookie exists
+ """
+ req = self._get_GET_csrf_cookie_request()
+ CsrfMiddleware().process_view(req, post_form_view, (), {})
+
+ resp = post_form_response()
+ resp_content = resp.content # needed because process_response modifies resp
+ resp2 = CsrfMiddleware().process_response(req, resp)
+ self.assertNotEqual(resp_content, resp2.content)
+ self._check_token_present(resp2)
+
+ def test_process_response_non_html(self):
+ """
+ Check the the post-processor does nothing for content-types not in _HTML_TYPES.
+ """
+ req = self._get_GET_no_csrf_cookie_request()
+ CsrfMiddleware().process_view(req, post_form_view, (), {})
+ resp = post_form_response_non_html()
+ resp_content = resp.content # needed because process_response modifies resp
+ resp2 = CsrfMiddleware().process_response(req, resp)
+ self.assertEquals(resp_content, resp2.content)
+
+ def test_process_response_exempt_view(self):
+ """
+ Check that no post processing is done for an exempt view
+ """
+ req = self._get_POST_csrf_cookie_request()
+ resp = csrf_exempt(post_form_view)(req)
+ resp_content = resp.content
+ resp2 = CsrfMiddleware().process_response(req, resp)
+ self.assertEquals(resp_content, resp2.content)
+
+ # Check the request processing
+ def test_process_request_no_session_no_csrf_cookie(self):
+ """
+ Check that if neither a CSRF cookie nor a session cookie are present,
+ the middleware rejects the incoming request. This will stop login CSRF.
+ """
+ req = self._get_POST_no_csrf_cookie_request()
+ req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
+ self.assertEquals(403, req2.status_code)
+
+ def test_process_request_csrf_cookie_no_token(self):
+ """
+ Check that if a CSRF cookie is present but no token, the middleware
+ rejects the incoming request.
+ """
+ req = self._get_POST_csrf_cookie_request()
+ req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
+ self.assertEquals(403, req2.status_code)
+
+ def test_process_request_csrf_cookie_and_token(self):
+ """
+ Check that if both a cookie and a token is present, the middleware lets it through.
+ """
+ req = self._get_POST_request_with_token()
+ req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
+ self.assertEquals(None, req2)
+
+ def test_process_request_session_cookie_no_csrf_cookie_token(self):
+ """
+ When no CSRF cookie exists, but the user has a session, check that a token
+ using the session cookie as a legacy CSRF cookie is accepted.
+ """
+ orig_secret_key = settings.SECRET_KEY
+ settings.SECRET_KEY = self._secret_key_for_session_test
+ try:
+ req = self._get_POST_session_request_with_token()
+ req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
+ self.assertEquals(None, req2)
+ finally:
+ settings.SECRET_KEY = orig_secret_key
+
+ def test_process_request_session_cookie_no_csrf_cookie_no_token(self):
+ """
+ Check that if a session cookie is present but no token and no CSRF cookie,
+ the request is rejected.
+ """
+ req = self._get_POST_session_request_no_token()
+ req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
+ self.assertEquals(403, req2.status_code)
+
+ def test_process_request_csrf_cookie_no_token_exempt_view(self):
+ """
+ Check that if a CSRF cookie is present and no token, but the csrf_exempt
+ decorator has been applied to the view, the middleware lets it through
+ """
+ req = self._get_POST_csrf_cookie_request()
+ req2 = CsrfMiddleware().process_view(req, csrf_exempt(post_form_view), (), {})
+ self.assertEquals(None, req2)
+
+ def test_ajax_exemption(self):
+ """
+ Check that AJAX requests are automatically exempted.
+ """
+ req = self._get_POST_csrf_cookie_request()
+ req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
+ req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
+ self.assertEquals(None, req2)
+
+ # Tests for the template tag method
+ def test_token_node_no_csrf_cookie(self):
+ """
+ Check that CsrfTokenNode works when no CSRF cookie is set
+ """
+ req = self._get_GET_no_csrf_cookie_request()
+ resp = token_view(req)
+ self.assertEquals(u"", resp.content)
+
+ def test_token_node_with_csrf_cookie(self):
+ """
+ Check that CsrfTokenNode works when a CSRF cookie is set
+ """
+ req = self._get_GET_csrf_cookie_request()
+ CsrfViewMiddleware().process_view(req, token_view, (), {})
+ resp = token_view(req)
+ self._check_token_present(resp)
+
+ def test_token_node_with_new_csrf_cookie(self):
+ """
+ Check that CsrfTokenNode works when a CSRF cookie is created by
+ the middleware (when one was not already present)
+ """
+ req = self._get_GET_no_csrf_cookie_request()
+ CsrfViewMiddleware().process_view(req, token_view, (), {})
+ resp = token_view(req)
+ resp2 = CsrfViewMiddleware().process_response(req, resp)
+ csrf_cookie = resp2.cookies[settings.CSRF_COOKIE_NAME]
+ self._check_token_present(resp, csrf_id=csrf_cookie.value)
+
+ def test_response_middleware_without_view_middleware(self):
+ """
+ Check that CsrfResponseMiddleware finishes without error if the view middleware
+ has not been called, as is the case if a request middleware returns a response.
+ """
+ req = self._get_GET_no_csrf_cookie_request()
+ resp = post_form_view(req)
+ CsrfMiddleware().process_response(req, resp)
+
+ def test_https_bad_referer(self):
+ """
+ Test that a POST HTTPS request with a bad referer is rejected
+ """
+ req = self._get_POST_request_with_token()
+ req._is_secure = True
+ req.META['HTTP_HOST'] = 'www.example.com'
+ req.META['HTTP_REFERER'] = 'https://www.evil.org/somepage'
+ req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+ self.assertNotEqual(None, req2)
+ self.assertEquals(403, req2.status_code)
+
+ def test_https_good_referer(self):
+ """
+ Test that a POST HTTPS request with a good referer is accepted
+ """
+ req = self._get_POST_request_with_token()
+ req._is_secure = True
+ req.META['HTTP_HOST'] = 'www.example.com'
+ req.META['HTTP_REFERER'] = 'https://www.example.com/somepage'
+ req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {})
+ self.assertEquals(None, req2)
diff --git a/tests/regressiontests/dateformat/tests.py b/tests/regressiontests/dateformat/tests.py
index d649d4789c..bbde45b903 100644
--- a/tests/regressiontests/dateformat/tests.py
+++ b/tests/regressiontests/dateformat/tests.py
@@ -1,90 +1,92 @@
-r"""
->>> format(my_birthday, '')
-u''
->>> format(my_birthday, 'a')
-u'p.m.'
->>> format(my_birthday, 'A')
-u'PM'
->>> format(my_birthday, 'd')
-u'08'
->>> format(my_birthday, 'j')
-u'8'
->>> format(my_birthday, 'l')
-u'Sunday'
->>> format(my_birthday, 'L')
-u'False'
->>> format(my_birthday, 'm')
-u'07'
->>> format(my_birthday, 'M')
-u'Jul'
->>> format(my_birthday, 'b')
-u'jul'
->>> format(my_birthday, 'n')
-u'7'
->>> format(my_birthday, 'N')
-u'July'
->>> no_tz or format(my_birthday, 'O') == '+0100'
-True
->>> format(my_birthday, 'P')
-u'10 p.m.'
->>> no_tz or format(my_birthday, 'r') == 'Sun, 8 Jul 1979 22:00:00 +0100'
-True
->>> format(my_birthday, 's')
-u'00'
->>> format(my_birthday, 'S')
-u'th'
->>> format(my_birthday, 't')
-u'31'
->>> no_tz or format(my_birthday, 'T') == 'CET'
-True
->>> no_tz or format(my_birthday, 'U') == '300315600'
-True
->>> format(my_birthday, 'w')
-u'0'
->>> format(my_birthday, 'W')
-u'27'
->>> format(my_birthday, 'y')
-u'79'
->>> format(my_birthday, 'Y')
-u'1979'
->>> format(my_birthday, 'z')
-u'189'
->>> no_tz or format(my_birthday, 'Z') == '3600'
-True
->>> no_tz or format(summertime, 'I') == '1'
-True
->>> no_tz or format(summertime, 'O') == '+0200'
-True
->>> no_tz or format(wintertime, 'I') == '0'
-True
->>> no_tz or format(wintertime, 'O') == '+0100'
-True
+from django.utils import dateformat, translation
+from unittest import TestCase
+import datetime, os, time
->>> format(my_birthday, r'Y z \C\E\T')
-u'1979 189 CET'
+class DateFormatTests(TestCase):
+ def setUp(self):
+ self.old_TZ = os.environ.get('TZ')
+ os.environ['TZ'] = 'Europe/Copenhagen'
+ translation.activate('en-us')
->>> format(my_birthday, r'jS o\f F')
-u'8th of July'
+ try:
+ # Check if a timezone has been set
+ time.tzset()
+ self.tz_tests = True
+ except AttributeError:
+ # No timezone available. Don't run the tests that require a TZ
+ self.tz_tests = False
->>> format(the_future, r'Y')
-u'2100'
-"""
+ def tearDown(self):
+ if self.old_TZ is None:
+ del os.environ['TZ']
+ else:
+ os.environ['TZ'] = self.old_TZ
-from django.utils import dateformat, translation
-import datetime, os, time
+ # Cleanup - force re-evaluation of TZ environment variable.
+ if self.tz_tests:
+ time.tzset()
+
+ def test_empty_format(self):
+ my_birthday = datetime.datetime(1979, 7, 8, 22, 00)
+
+ self.assertEquals(dateformat.format(my_birthday, ''), u'')
+
+ def test_am_pm(self):
+ my_birthday = datetime.datetime(1979, 7, 8, 22, 00)
+
+ self.assertEquals(dateformat.format(my_birthday, 'a'), u'p.m.')
+
+ def test_date_formats(self):
+ my_birthday = datetime.datetime(1979, 7, 8, 22, 00)
+
+ self.assertEquals(dateformat.format(my_birthday, 'A'), u'PM')
+ self.assertEquals(dateformat.format(my_birthday, 'd'), u'08')
+ self.assertEquals(dateformat.format(my_birthday, 'j'), u'8')
+ self.assertEquals(dateformat.format(my_birthday, 'l'), u'Sunday')
+ self.assertEquals(dateformat.format(my_birthday, 'L'), u'False')
+ self.assertEquals(dateformat.format(my_birthday, 'm'), u'07')
+ self.assertEquals(dateformat.format(my_birthday, 'M'), u'Jul')
+ self.assertEquals(dateformat.format(my_birthday, 'b'), u'jul')
+ self.assertEquals(dateformat.format(my_birthday, 'n'), u'7')
+ self.assertEquals(dateformat.format(my_birthday, 'N'), u'July')
+
+ def test_time_formats(self):
+ my_birthday = datetime.datetime(1979, 7, 8, 22, 00)
+
+ self.assertEquals(dateformat.format(my_birthday, 'P'), u'10 p.m.')
+ self.assertEquals(dateformat.format(my_birthday, 's'), u'00')
+ self.assertEquals(dateformat.format(my_birthday, 'S'), u'th')
+ self.assertEquals(dateformat.format(my_birthday, 't'), u'31')
+ self.assertEquals(dateformat.format(my_birthday, 'w'), u'0')
+ self.assertEquals(dateformat.format(my_birthday, 'W'), u'27')
+ self.assertEquals(dateformat.format(my_birthday, 'y'), u'79')
+ self.assertEquals(dateformat.format(my_birthday, 'Y'), u'1979')
+ self.assertEquals(dateformat.format(my_birthday, 'z'), u'189')
+
+ def test_dateformat(self):
+ my_birthday = datetime.datetime(1979, 7, 8, 22, 00)
+
+ self.assertEquals(dateformat.format(my_birthday, r'Y z \C\E\T'), u'1979 189 CET')
+
+ self.assertEquals(dateformat.format(my_birthday, r'jS o\f F'), u'8th of July')
-format = dateformat.format
-os.environ['TZ'] = 'Europe/Copenhagen'
-translation.activate('en-us')
+ def test_futuredates(self):
+ the_future = datetime.datetime(2100, 10, 25, 0, 00)
+ self.assertEquals(dateformat.format(the_future, r'Y'), u'2100')
-try:
- time.tzset()
- no_tz = False
-except AttributeError:
- no_tz = True
+ def test_timezones(self):
+ my_birthday = datetime.datetime(1979, 7, 8, 22, 00)
+ summertime = datetime.datetime(2005, 10, 30, 1, 00)
+ wintertime = datetime.datetime(2005, 10, 30, 4, 00)
-my_birthday = datetime.datetime(1979, 7, 8, 22, 00)
-summertime = datetime.datetime(2005, 10, 30, 1, 00)
-wintertime = datetime.datetime(2005, 10, 30, 4, 00)
-the_future = datetime.datetime(2100, 10, 25, 0, 00)
+ if self.tz_tests:
+ self.assertEquals(dateformat.format(my_birthday, 'O'), u'+0100')
+ self.assertEquals(dateformat.format(my_birthday, 'r'), u'Sun, 8 Jul 1979 22:00:00 +0100')
+ self.assertEquals(dateformat.format(my_birthday, 'T'), u'CET')
+ self.assertEquals(dateformat.format(my_birthday, 'U'), u'300315600')
+ self.assertEquals(dateformat.format(my_birthday, 'Z'), u'3600')
+ self.assertEquals(dateformat.format(summertime, 'I'), u'1')
+ self.assertEquals(dateformat.format(summertime, 'O'), u'+0200')
+ self.assertEquals(dateformat.format(wintertime, 'I'), u'0')
+ self.assertEquals(dateformat.format(wintertime, 'O'), u'+0100')
diff --git a/tests/regressiontests/m2m_through_regress/models.py b/tests/regressiontests/m2m_through_regress/models.py
index dcf5f8115b..56aecd6975 100644
--- a/tests/regressiontests/m2m_through_regress/models.py
+++ b/tests/regressiontests/m2m_through_regress/models.py
@@ -84,22 +84,22 @@ __test__ = {'API_TESTS':"""
>>> bob.group_set = []
Traceback (most recent call last):
...
-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
>>> roll.members = []
Traceback (most recent call last):
...
-AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+AttributeError: Cannot set values on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
>>> rock.members.create(name='Anne')
Traceback (most recent call last):
...
-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
>>> bob.group_set.create(name='Funk')
Traceback (most recent call last):
...
-AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead.
+AttributeError: Cannot use create() on a ManyToManyField which specifies an intermediary model. Use m2m_through_regress.Membership's Manager instead.
# Now test that the intermediate with a relationship outside
# the current app (i.e., UserMembership) workds
diff --git a/tests/regressiontests/mail/custombackend.py b/tests/regressiontests/mail/custombackend.py
new file mode 100644
index 0000000000..6b0e15ad0a
--- /dev/null
+++ b/tests/regressiontests/mail/custombackend.py
@@ -0,0 +1,15 @@
+"""A custom backend for testing."""
+
+from django.core.mail.backends.base import BaseEmailBackend
+
+
+class EmailBackend(BaseEmailBackend):
+
+ def __init__(self, *args, **kwargs):
+ super(EmailBackend, self).__init__(*args, **kwargs)
+ self.test_outbox = []
+
+ def send_messages(self, email_messages):
+ # Messages are stored in a instance variable for testing.
+ self.test_outbox.extend(email_messages)
+ return len(email_messages)
diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py
index e90d77366f..10d2ae7df3 100644
--- a/tests/regressiontests/mail/tests.py
+++ b/tests/regressiontests/mail/tests.py
@@ -1,10 +1,18 @@
# coding: utf-8
+
r"""
# Tests for the django.core.mail.
+>>> import os
+>>> import shutil
+>>> import tempfile
+>>> from StringIO import StringIO
>>> from django.conf import settings
>>> from django.core import mail
>>> from django.core.mail import EmailMessage, mail_admins, mail_managers, EmailMultiAlternatives
+>>> from django.core.mail import send_mail, send_mass_mail
+>>> from django.core.mail.backends.base import BaseEmailBackend
+>>> from django.core.mail.backends import console, dummy, locmem, filebased, smtp
>>> from django.utils.translation import ugettext_lazy
# Test normal ascii character case:
@@ -85,8 +93,6 @@ BadHeaderError: Header values can't contain newlines (got u'Subject\nInjection T
>>> mail_managers('hi','there')
>>> len(mail.outbox)
1
->>> settings.ADMINS = old_admins
->>> settings.MANAGERS = old_managers
# Make sure we can manually set the From header (#9214)
@@ -95,6 +101,17 @@ BadHeaderError: Header values can't contain newlines (got u'Subject\nInjection T
>>> message['From']
'from@example.com'
+# Regression for #11144 - When a to/from/cc header contains unicode,
+# make sure the email addresses are parsed correctly (especially
+# with regards to commas)
+>>> email = EmailMessage('Subject', 'Content', 'from@example.com', ['"Firstname Sürname" <to@example.com>','other@example.com'])
+>>> email.message()['To']
+'=?utf-8?q?Firstname_S=C3=BCrname?= <to@example.com>, other@example.com'
+
+>>> email = EmailMessage('Subject', 'Content', 'from@example.com', ['"Sürname, Firstname" <to@example.com>','other@example.com'])
+>>> email.message()['To']
+'=?utf-8?q?S=C3=BCrname=2C_Firstname?= <to@example.com>, other@example.com'
+
# Handle attachments within an multipart/alternative mail correctly (#9367)
# (test is not as precise/clear as it could be w.r.t. email tree structure,
# but it's good enough.)
@@ -138,4 +155,217 @@ Content-Disposition: attachment; filename="an attachment.pdf"
JVBERi0xLjQuJS4uLg==
...
+# Make sure that the console backend writes to stdout by default
+>>> connection = console.EmailBackend()
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection.send_messages([email])
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject
+From: from@example.com
+To: to@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+1
+
+# Test that the console backend can be pointed at an arbitrary stream
+>>> s = StringIO()
+>>> connection = mail.get_connection('django.core.mail.backends.console', stream=s)
+>>> send_mail('Subject', 'Content', 'from@example.com', ['to@example.com'], connection=connection)
+1
+>>> print s.getvalue()
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject
+From: from@example.com
+To: to@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+
+# Make sure that dummy backends returns correct number of sent messages
+>>> connection = dummy.EmailBackend()
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection.send_messages([email, email, email])
+3
+
+# Make sure that locmen backend populates the outbox
+>>> mail.outbox = []
+>>> connection = locmem.EmailBackend()
+>>> email1 = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> email2 = EmailMessage('Subject 2', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection.send_messages([email1, email2])
+2
+>>> len(mail.outbox)
+2
+>>> mail.outbox[0].subject
+'Subject'
+>>> mail.outbox[1].subject
+'Subject 2'
+
+# Make sure that multiple locmem connections share mail.outbox
+>>> mail.outbox = []
+>>> connection1 = locmem.EmailBackend()
+>>> connection2 = locmem.EmailBackend()
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection1.send_messages([email])
+1
+>>> connection2.send_messages([email])
+1
+>>> len(mail.outbox)
+2
+
+# Make sure that the file backend write to the right location
+>>> tmp_dir = tempfile.mkdtemp()
+>>> connection = filebased.EmailBackend(file_path=tmp_dir)
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection.send_messages([email])
+1
+>>> len(os.listdir(tmp_dir))
+1
+>>> print open(os.path.join(tmp_dir, os.listdir(tmp_dir)[0])).read()
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject
+From: from@example.com
+To: to@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+
+>>> connection2 = filebased.EmailBackend(file_path=tmp_dir)
+>>> connection2.send_messages([email])
+1
+>>> len(os.listdir(tmp_dir))
+2
+>>> connection.send_messages([email])
+1
+>>> len(os.listdir(tmp_dir))
+2
+>>> email.connection = filebased.EmailBackend(file_path=tmp_dir)
+>>> connection_created = connection.open()
+>>> num_sent = email.send()
+>>> len(os.listdir(tmp_dir))
+3
+>>> num_sent = email.send()
+>>> len(os.listdir(tmp_dir))
+3
+>>> connection.close()
+>>> shutil.rmtree(tmp_dir)
+
+# Make sure that get_connection() accepts arbitrary keyword that might be
+# used with custom backends.
+>>> c = mail.get_connection(fail_silently=True, foo='bar')
+>>> c.fail_silently
+True
+
+# Test custom backend defined in this suite.
+>>> conn = mail.get_connection('regressiontests.mail.custombackend')
+>>> hasattr(conn, 'test_outbox')
+True
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> conn.send_messages([email])
+1
+>>> len(conn.test_outbox)
+1
+
+# Test backend argument of mail.get_connection()
+>>> isinstance(mail.get_connection('django.core.mail.backends.smtp'), smtp.EmailBackend)
+True
+>>> isinstance(mail.get_connection('django.core.mail.backends.locmem'), locmem.EmailBackend)
+True
+>>> isinstance(mail.get_connection('django.core.mail.backends.dummy'), dummy.EmailBackend)
+True
+>>> isinstance(mail.get_connection('django.core.mail.backends.console'), console.EmailBackend)
+True
+>>> tmp_dir = tempfile.mkdtemp()
+>>> isinstance(mail.get_connection('django.core.mail.backends.filebased', file_path=tmp_dir), filebased.EmailBackend)
+True
+>>> shutil.rmtree(tmp_dir)
+>>> isinstance(mail.get_connection(), locmem.EmailBackend)
+True
+
+# Test connection argument of send_mail() et al
+>>> connection = mail.get_connection('django.core.mail.backends.console')
+>>> send_mail('Subject', 'Content', 'from@example.com', ['to@example.com'], connection=connection)
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject
+From: from@example.com
+To: to@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+1
+
+>>> send_mass_mail([
+... ('Subject1', 'Content1', 'from1@example.com', ['to1@example.com']),
+... ('Subject2', 'Content2', 'from2@example.com', ['to2@example.com'])
+... ], connection=connection)
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject1
+From: from1@example.com
+To: to1@example.com
+Date: ...
+Message-ID: ...
+
+Content1
+-------------------------------------------------------------------------------
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject2
+From: from2@example.com
+To: to2@example.com
+Date: ...
+Message-ID: ...
+
+Content2
+-------------------------------------------------------------------------------
+2
+
+>>> mail_admins('Subject', 'Content', connection=connection)
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: [Django] Subject
+From: root@localhost
+To: nobody@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+
+>>> mail_managers('Subject', 'Content', connection=connection)
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: [Django] Subject
+From: root@localhost
+To: nobody@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+
+>>> settings.ADMINS = old_admins
+>>> settings.MANAGERS = old_managers
+
"""
diff --git a/tests/regressiontests/model_formsets_regress/tests.py b/tests/regressiontests/model_formsets_regress/tests.py
index f5e9355e42..4dba9fc19f 100644
--- a/tests/regressiontests/model_formsets_regress/tests.py
+++ b/tests/regressiontests/model_formsets_regress/tests.py
@@ -140,3 +140,13 @@ class InlineFormsetTests(TestCase):
self.assertEqual(manager[1]['name'], 'Terry Gilliam')
else:
self.fail('Errors found on formset:%s' % form_set.errors)
+
+ def test_formset_with_none_instance(self):
+ "A formset with instance=None can be created. Regression for #11872"
+ Form = modelform_factory(User)
+ FormSet = inlineformset_factory(User, UserSite)
+
+ # Instantiate the Form and FormSet to prove
+ # you can create a formset with an instance of None
+ form = Form(instance=None)
+ formset = FormSet(instance=None)
diff --git a/tests/regressiontests/model_inheritance_regress/models.py b/tests/regressiontests/model_inheritance_regress/models.py
index a1ee6a2d86..6a804a97c1 100644
--- a/tests/regressiontests/model_inheritance_regress/models.py
+++ b/tests/regressiontests/model_inheritance_regress/models.py
@@ -110,6 +110,36 @@ class DerivedM(BaseM):
return "PK = %d, base_name = %s, derived_name = %s" \
% (self.customPK, self.base_name, self.derived_name)
+# Check that abstract classes don't get m2m tables autocreated.
+class Person(models.Model):
+ name = models.CharField(max_length=100)
+
+ class Meta:
+ ordering = ('name',)
+
+ def __unicode__(self):
+ return self.name
+
+class AbstractEvent(models.Model):
+ name = models.CharField(max_length=100)
+ attendees = models.ManyToManyField(Person, related_name="%(class)s_set")
+
+ class Meta:
+ abstract = True
+ ordering = ('name',)
+
+ def __unicode__(self):
+ return self.name
+
+class BirthdayParty(AbstractEvent):
+ pass
+
+class BachelorParty(AbstractEvent):
+ pass
+
+class MessyBachelorParty(BachelorParty):
+ pass
+
__test__ = {'API_TESTS':"""
# Regression for #7350, #7202
# Check that when you create a Parent object with a specific reference to an
@@ -318,5 +348,41 @@ True
>>> ParkingLot3._meta.get_ancestor_link(Place).name # the child->parent link
"parent"
+# Check that many-to-many relations defined on an abstract base class
+# are correctly inherited (and created) on the child class.
+>>> p1 = Person.objects.create(name='Alice')
+>>> p2 = Person.objects.create(name='Bob')
+>>> p3 = Person.objects.create(name='Carol')
+>>> p4 = Person.objects.create(name='Dave')
+
+>>> birthday = BirthdayParty.objects.create(name='Birthday party for Alice')
+>>> birthday.attendees = [p1, p3]
+
+>>> bachelor = BachelorParty.objects.create(name='Bachelor party for Bob')
+>>> bachelor.attendees = [p2, p4]
+
+>>> print p1.birthdayparty_set.all()
+[<BirthdayParty: Birthday party for Alice>]
+
+>>> print p1.bachelorparty_set.all()
+[]
+
+>>> print p2.bachelorparty_set.all()
+[<BachelorParty: Bachelor party for Bob>]
+
+# Check that a subclass of a subclass of an abstract model
+# doesn't get it's own accessor.
+>>> p2.messybachelorparty_set.all()
+Traceback (most recent call last):
+...
+AttributeError: 'Person' object has no attribute 'messybachelorparty_set'
+
+# ... but it does inherit the m2m from it's parent
+>>> messy = MessyBachelorParty.objects.create(name='Bachelor party for Dave')
+>>> messy.attendees = [p4]
+
+>>> p4.bachelorparty_set.all()
+[<BachelorParty: Bachelor party for Bob>, <BachelorParty: Bachelor party for Dave>]
+
"""}
diff --git a/tests/regressiontests/serializers_regress/models.py b/tests/regressiontests/serializers_regress/models.py
index 95119d4b05..313ed8fc3a 100644
--- a/tests/regressiontests/serializers_regress/models.py
+++ b/tests/regressiontests/serializers_regress/models.py
@@ -105,6 +105,9 @@ class Anchor(models.Model):
data = models.CharField(max_length=30)
+ class Meta:
+ ordering = ('id',)
+
class UniqueAnchor(models.Model):
"""This is a model that can be used as
something for other models to point at"""
@@ -135,7 +138,7 @@ class FKDataToO2O(models.Model):
class M2MIntermediateData(models.Model):
data = models.ManyToManyField(Anchor, null=True, through='Intermediate')
-
+
class Intermediate(models.Model):
left = models.ForeignKey(M2MIntermediateData)
right = models.ForeignKey(Anchor)
@@ -242,7 +245,7 @@ class AbstractBaseModel(models.Model):
class InheritAbstractModel(AbstractBaseModel):
child_data = models.IntegerField()
-
+
class BaseModel(models.Model):
parent_data = models.IntegerField()
@@ -252,4 +255,3 @@ class InheritBaseModel(BaseModel):
class ExplicitInheritBaseModel(BaseModel):
parent = models.OneToOneField(BaseModel)
child_data = models.IntegerField()
- \ No newline at end of file
diff --git a/tests/regressiontests/signals_regress/__init__.py b/tests/regressiontests/signals_regress/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/regressiontests/signals_regress/__init__.py
diff --git a/tests/regressiontests/signals_regress/models.py b/tests/regressiontests/signals_regress/models.py
new file mode 100644
index 0000000000..2b40ebc21a
--- /dev/null
+++ b/tests/regressiontests/signals_regress/models.py
@@ -0,0 +1,89 @@
+"""
+Testing signals before/after saving and deleting.
+"""
+
+from django.db import models
+
+class Author(models.Model):
+ name = models.CharField(max_length=20)
+
+ def __unicode__(self):
+ return self.name
+
+class Book(models.Model):
+ name = models.CharField(max_length=20)
+ authors = models.ManyToManyField(Author)
+
+ def __unicode__(self):
+ return self.name
+
+def pre_save_test(signal, sender, instance, **kwargs):
+ print 'pre_save signal,', instance
+ if kwargs.get('raw'):
+ print 'Is raw'
+
+def post_save_test(signal, sender, instance, **kwargs):
+ print 'post_save signal,', instance
+ if 'created' in kwargs:
+ if kwargs['created']:
+ print 'Is created'
+ else:
+ print 'Is updated'
+ if kwargs.get('raw'):
+ print 'Is raw'
+
+def pre_delete_test(signal, sender, instance, **kwargs):
+ print 'pre_delete signal,', instance
+ print 'instance.id is not None: %s' % (instance.id != None)
+
+def post_delete_test(signal, sender, instance, **kwargs):
+ print 'post_delete signal,', instance
+ print 'instance.id is not None: %s' % (instance.id != None)
+
+__test__ = {'API_TESTS':"""
+
+# Save up the number of connected signals so that we can check at the end
+# that all the signals we register get properly unregistered (#9989)
+>>> pre_signals = (len(models.signals.pre_save.receivers),
+... len(models.signals.post_save.receivers),
+... len(models.signals.pre_delete.receivers),
+... len(models.signals.post_delete.receivers))
+
+>>> models.signals.pre_save.connect(pre_save_test)
+>>> models.signals.post_save.connect(post_save_test)
+>>> models.signals.pre_delete.connect(pre_delete_test)
+>>> models.signals.post_delete.connect(post_delete_test)
+
+>>> a1 = Author(name='Neal Stephenson')
+>>> a1.save()
+pre_save signal, Neal Stephenson
+post_save signal, Neal Stephenson
+Is created
+
+>>> b1 = Book(name='Snow Crash')
+>>> b1.save()
+pre_save signal, Snow Crash
+post_save signal, Snow Crash
+Is created
+
+# Assigning to m2m shouldn't generate an m2m signal
+>>> b1.authors = [a1]
+
+# Removing an author from an m2m shouldn't generate an m2m signal
+>>> b1.authors = []
+
+>>> models.signals.post_delete.disconnect(post_delete_test)
+>>> models.signals.pre_delete.disconnect(pre_delete_test)
+>>> models.signals.post_save.disconnect(post_save_test)
+>>> models.signals.pre_save.disconnect(pre_save_test)
+
+# Check that all our signals got disconnected properly.
+>>> post_signals = (len(models.signals.pre_save.receivers),
+... len(models.signals.post_save.receivers),
+... len(models.signals.pre_delete.receivers),
+... len(models.signals.post_delete.receivers))
+
+>>> pre_signals == post_signals
+True
+
+"""}
diff --git a/tests/regressiontests/test_client_regress/models.py b/tests/regressiontests/test_client_regress/models.py
index be2cb8fa37..58693cc395 100644
--- a/tests/regressiontests/test_client_regress/models.py
+++ b/tests/regressiontests/test_client_regress/models.py
@@ -574,6 +574,23 @@ class RequestMethodTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, 'request method: DELETE')
+class RequestMethodStringDataTests(TestCase):
+ def test_post(self):
+ "Request a view with string data via request method POST"
+ # Regression test for #11371
+ data = u'{"test": "json"}'
+ response = self.client.post('/test_client_regress/request_methods/', data=data, content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, 'request method: POST')
+
+ def test_put(self):
+ "Request a view with string data via request method PUT"
+ # Regression test for #11371
+ data = u'{"test": "json"}'
+ response = self.client.put('/test_client_regress/request_methods/', data=data, content_type='application/json')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content, 'request method: PUT')
+
class QueryStringTests(TestCase):
def test_get_like_requests(self):
for method_name in ('get','head','options','put','delete'):
diff --git a/tests/regressiontests/utils/dateformat.py b/tests/regressiontests/utils/dateformat.py
index 63b8201752..b80010f8d8 100644
--- a/tests/regressiontests/utils/dateformat.py
+++ b/tests/regressiontests/utils/dateformat.py
@@ -1,48 +1,35 @@
-"""
->>> from datetime import datetime, date
->>> from django.utils.dateformat import format
->>> from django.utils.tzinfo import FixedOffset, LocalTimezone
+import os
+from unittest import TestCase
+from datetime import datetime, date
+from django.utils.dateformat import format
+from django.utils.tzinfo import FixedOffset, LocalTimezone
-# date
->>> d = date(2009, 5, 16)
->>> date.fromtimestamp(int(format(d, 'U'))) == d
-True
+class DateFormatTests(TestCase):
+ def test_date(self):
+ d = date(2009, 5, 16)
+ self.assertEquals(date.fromtimestamp(int(format(d, 'U'))), d)
-# Naive datetime
->>> dt = datetime(2009, 5, 16, 5, 30, 30)
->>> datetime.fromtimestamp(int(format(dt, 'U'))) == dt
-True
+ def test_naive_datetime(self):
+ dt = datetime(2009, 5, 16, 5, 30, 30)
+ self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U'))), dt)
-# datetime with local tzinfo
->>> ltz = LocalTimezone(datetime.now())
->>> dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=ltz)
->>> datetime.fromtimestamp(int(format(dt, 'U')), ltz) == dt
-True
->>> datetime.fromtimestamp(int(format(dt, 'U'))) == dt.replace(tzinfo=None)
-True
+ def test_datetime_with_local_tzinfo(self):
+ ltz = LocalTimezone(datetime.now())
+ dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=ltz)
+ self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), ltz), dt)
+ self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U'))), dt.replace(tzinfo=None))
-# datetime with arbitrary tzinfo
->>> tz = FixedOffset(-510)
->>> ltz = LocalTimezone(datetime.now())
->>> dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=tz)
->>> datetime.fromtimestamp(int(format(dt, 'U')), tz) == dt
-True
->>> datetime.fromtimestamp(int(format(dt, 'U')), ltz) == dt
-True
->>> datetime.fromtimestamp(int(format(dt, 'U'))) == dt.astimezone(ltz).replace(tzinfo=None)
-True
->>> datetime.fromtimestamp(int(format(dt, 'U')), tz).utctimetuple() == dt.utctimetuple()
-True
->>> datetime.fromtimestamp(int(format(dt, 'U')), ltz).utctimetuple() == dt.utctimetuple()
-True
+ def test_datetime_with_tzinfo(self):
+ tz = FixedOffset(-510)
+ ltz = LocalTimezone(datetime.now())
+ dt = datetime(2009, 5, 16, 5, 30, 30, tzinfo=tz)
+ self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), tz), dt)
+ self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), ltz), dt)
+ self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U'))), dt.astimezone(ltz).replace(tzinfo=None))
+ self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), tz).utctimetuple(), dt.utctimetuple())
+ self.assertEquals(datetime.fromtimestamp(int(format(dt, 'U')), ltz).utctimetuple(), dt.utctimetuple())
-# Epoch
->>> utc = FixedOffset(0)
->>> udt = datetime(1970, 1, 1, tzinfo=utc)
->>> format(udt, 'U')
-u'0'
-"""
-
-if __name__ == "__main__":
- import doctest
- doctest.testmod()
+ def test_epoch(self):
+ utc = FixedOffset(0)
+ udt = datetime(1970, 1, 1, tzinfo=utc)
+ self.assertEquals(format(udt, 'U'), u'0')
diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py
index 6d345d9afb..a7a6e4c3a1 100644
--- a/tests/regressiontests/utils/tests.py
+++ b/tests/regressiontests/utils/tests.py
@@ -5,6 +5,7 @@ Tests for django.utils.
from unittest import TestCase
from django.utils import html, checksums
+from django.utils.functional import SimpleLazyObject
import timesince
import datastructures
@@ -23,10 +24,11 @@ except NameError:
__test__ = {
'timesince': timesince,
'datastructures': datastructures,
- 'dateformat': dateformat,
'itercompat': itercompat,
}
+from dateformat import *
+
class TestUtilsHtml(TestCase):
def check_output(self, function, value, output=None):
@@ -161,6 +163,80 @@ class TestUtilsChecksums(TestCase):
for value, output in items:
self.check_output(f, value, output)
+class _ComplexObject(object):
+ def __init__(self, name):
+ self.name = name
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+ def __hash__(self):
+ return hash(self.name)
+
+ def __str__(self):
+ return "I am _ComplexObject(%r)" % self.name
+
+ def __unicode__(self):
+ return unicode(self.name)
+
+ def __repr__(self):
+ return "_ComplexObject(%r)" % self.name
+
+complex_object = lambda: _ComplexObject("joe")
+
+class TestUtilsSimpleLazyObject(TestCase):
+ """
+ Tests for SimpleLazyObject
+ """
+ # Note that concrete use cases for SimpleLazyObject are also found in the
+ # auth context processor tests (unless the implementation of that function
+ # is changed).
+
+ def test_equality(self):
+ self.assertEqual(complex_object(), SimpleLazyObject(complex_object))
+ self.assertEqual(SimpleLazyObject(complex_object), complex_object())
+
+ def test_hash(self):
+ # hash() equality would not be true for many objects, but it should be
+ # for _ComplexObject
+ self.assertEqual(hash(complex_object()),
+ hash(SimpleLazyObject(complex_object)))
+
+ def test_repr(self):
+ # For debugging, it will really confuse things if there is no clue that
+ # SimpleLazyObject is actually a proxy object. So we don't
+ # proxy __repr__
+ self.assert_("SimpleLazyObject" in repr(SimpleLazyObject(complex_object)))
+
+ def test_str(self):
+ self.assertEqual("I am _ComplexObject('joe')", str(SimpleLazyObject(complex_object)))
+
+ def test_unicode(self):
+ self.assertEqual(u"joe", unicode(SimpleLazyObject(complex_object)))
+
+ def test_class(self):
+ # This is important for classes that use __class__ in things like
+ # equality tests.
+ self.assertEqual(_ComplexObject, SimpleLazyObject(complex_object).__class__)
+
+ def test_deepcopy(self):
+ import copy
+ # Check that we *can* do deep copy, and that it returns the right
+ # objects.
+
+ # First, for an unevaluated SimpleLazyObject
+ s = SimpleLazyObject(complex_object)
+ assert s._wrapped is None
+ s2 = copy.deepcopy(s)
+ assert s._wrapped is None # something has gone wrong is s is evaluated
+ self.assertEqual(s2, complex_object())
+
+ # Second, for an evaluated SimpleLazyObject
+ name = s.name # evaluate
+ assert s._wrapped is not None
+ s3 = copy.deepcopy(s)
+ self.assertEqual(s3, complex_object())
+
if __name__ == "__main__":
import doctest
doctest.testmod()
diff --git a/tests/regressiontests/views/tests/generic/date_based.py b/tests/regressiontests/views/tests/generic/date_based.py
index e0bae28ed0..2ca1bfd090 100644
--- a/tests/regressiontests/views/tests/generic/date_based.py
+++ b/tests/regressiontests/views/tests/generic/date_based.py
@@ -100,7 +100,7 @@ class MonthArchiveTest(TestCase):
now = datetime.now()
prev_month = now.date().replace(day=1)
- if prev_month.month == 11:
+ if prev_month.month == 1:
prev_month = prev_month.replace(year=prev_month.year-1, month=12)
else:
prev_month = prev_month.replace(month=prev_month.month-1)
@@ -121,4 +121,4 @@ class DayArchiveTests(TestCase):
article = Article.objects.create(title="example", author=author, date_created=datetime(2004, 1, 21, 0, 0, 1))
response = self.client.get('/views/date_based/archive_day/2004/1/21/')
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context['object_list'][0], article) \ No newline at end of file
+ self.assertEqual(response.context['object_list'][0], article)