mirror of
https://github.com/django/django.git
synced 2025-07-06 02:39:12 +00:00
[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
This commit is contained in:
parent
dfe495fbe8
commit
30ea350dab
7
AUTHORS
7
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:
|
||||
|
||||
|
10
INSTALL
10
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.
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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 #
|
||||
###########
|
||||
|
Binary file not shown.
@ -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"
|
||||
|
@ -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',
|
||||
)
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
@ -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" %}" />
|
||||
|
@ -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 }}" />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 }}
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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 "
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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).
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
@ -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>
|
||||
|
@ -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'),
|
||||
)
|
||||
|
@ -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."""
|
||||
|
@ -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.
|
||||
|
||||
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
|
||||
|
||||
Actually perform the flagging of a comment from a request.
|
||||
"""
|
||||
qs = comments.get_model().objects.filter(is_public=False, is_removed=False)
|
||||
paginator = Paginator(qs, 100)
|
||||
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,
|
||||
)
|
||||
|
||||
try:
|
||||
page = int(request.GET.get("page", 1))
|
||||
except ValueError:
|
||||
raise Http404
|
||||
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,
|
||||
)
|
||||
|
||||
try:
|
||||
comments_per_page = paginator.page(page)
|
||||
except InvalidPage:
|
||||
raise Http404
|
||||
|
||||
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))
|
||||
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,
|
||||
)
|
||||
|
||||
moderation_queue = permission_required("comments.can_moderate")(moderation_queue)
|
||||
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,
|
||||
)
|
||||
|
||||
# Confirmation views.
|
||||
|
||||
flag_done = confirmation_view(
|
||||
template = "comments/flagged.html",
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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])
|
||||
|
@ -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)):
|
||||
|
@ -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
|
||||
|
@ -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).
|
||||
|
@ -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))
|
||||
|
@ -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:
|
||||
|
@ -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.")
|
||||
|
@ -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
|
||||
|
110
django/core/mail/__init__.py
Normal file
110
django/core/mail/__init__.py
Normal file
@ -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)
|
1
django/core/mail/backends/__init__.py
Normal file
1
django/core/mail/backends/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Mail backends shipped with Django.
|
39
django/core/mail/backends/base.py
Normal file
39
django/core/mail/backends/base.py
Normal file
@ -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
|
37
django/core/mail/backends/console.py
Normal file
37
django/core/mail/backends/console.py
Normal file
@ -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)
|
9
django/core/mail/backends/dummy.py
Normal file
9
django/core/mail/backends/dummy.py
Normal file
@ -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)
|
59
django/core/mail/backends/filebased.py
Normal file
59
django/core/mail/backends/filebased.py
Normal file
@ -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
|
||||
|
24
django/core/mail/backends/locmem.py
Normal file
24
django/core/mail/backends/locmem.py
Normal file
@ -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)
|
106
django/core/mail/backends/smtp.py
Normal file
106
django/core/mail/backends/smtp.py
Normal file
@ -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
|
@ -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)
|
19
django/core/mail/utils.py
Normal file
19
django/core/mail/utils.py
Normal file
@ -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()
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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()]
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
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)
|
||||
|
||||
# Return the newly cached value
|
||||
return self._m2m_column_name_cache
|
||||
|
||||
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
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
265
django/middleware/csrf.py
Normal file
265
django/middleware/csrf.py
Normal file
@ -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)
|
||||
|
@ -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__
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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('.')
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
69
django/views/csrf.py
Normal file
69
django/views/csrf.py
Normal file
@ -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')
|
47
django/views/decorators/csrf.py
Normal file
47
django/views/decorators/csrf.py
Normal file
@ -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))
|
@ -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
|
||||
|
@ -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
|
||||
-----------
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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_.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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`_.
|
||||
|
||||
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``.
|
||||
To enable CSRF protection for your views, follow these steps:
|
||||
|
||||
.. versionchanged:: 1.1
|
||||
(previous versions of Django did not provide these two components
|
||||
of ``CsrfMiddleware`` as described above)
|
||||
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``
|
||||
|
||||
====================================================== ==============================================
|
||||
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
|
||||
====================================================== ==============================================
|
||||
|
||||
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 next section.)
|
||||
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::
|
||||
|
||||
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. 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``).
|
||||
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.
|
||||
|
||||
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``)
|
||||
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).
|
||||
|
||||
This ensures that only forms that have originated from your Web site
|
||||
can be used to POST data back.
|
||||
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.
|
||||
|
||||
This part is done by the template tag (and with the legacy method, it is done
|
||||
by ``CsrfResponseMiddleware``).
|
||||
|
||||
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 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 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.
|
||||
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
|
||||
=========================
|
||||
|
||||
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.
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
-------------
|
||||
|
||||
|
@ -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`.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user