diff --git a/AUTHORS b/AUTHORS index 2135afcc7b..50ca0af541 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ The PRIMARY AUTHORS are (and/or have been): * Brian Rosner * Justin Bronn * Karen Tracey + * Jannis Leidel More information on the main contributors to Django can be found in docs/internals/committers.txt. @@ -26,6 +27,7 @@ answer newbie questions, and generally made Django that much better: ajs alang@bright-green.com + Andi Albrecht Marty Alchin Ahmad Alhashemi Daniel Alves Barbosa de Oliveira Vaz @@ -268,7 +270,6 @@ answer newbie questions, and generally made Django that much better: lcordier@point45.com Jeong-Min Lee Tai Lee - Jannis Leidel Christopher Lenz lerouxb@gmail.com Piotr Lewandowski @@ -423,7 +424,7 @@ answer newbie questions, and generally made Django that much better: Travis Terry thebjorn Zach Thompson - Michael Thornhill + Michael Thornhill Deepak Thukral tibimicu@gmx.net tobias@neuyork.de @@ -471,6 +472,8 @@ answer newbie questions, and generally made Django that much better: Gasper Zejn Jarek Zgoda Cheng Zhang + Glenn Maynard + bthomas A big THANK YOU goes to: diff --git a/INSTALL b/INSTALL index 9b79ec016b..644c524bbb 100644 --- a/INSTALL +++ b/INSTALL @@ -1,22 +1,16 @@ Thanks for downloading Django. -To install it, make sure you have Python 2.3 or greater installed. Then run +To install it, make sure you have Python 2.4 or greater installed. Then run this command from the command prompt: python setup.py install -Note this requires a working Internet connection if you don't already have the -Python utility "setuptools" installed. - AS AN ALTERNATIVE, you can just copy the entire "django" directory to Python's site-packages directory, which is located wherever your Python installation lives. Some places you might check are: + /usr/lib/python2.5/site-packages (Unix, Python 2.5) /usr/lib/python2.4/site-packages (Unix, Python 2.4) - /usr/lib/python2.3/site-packages (Unix, Python 2.3) C:\\PYTHON\site-packages (Windows) -This second solution does not require a working Internet connection; it -bypasses "setuptools" entirely. - For more detailed instructions, see docs/intro/install.txt. diff --git a/django/conf/__init__.py b/django/conf/__init__.py index 7bc7ae9508..7fbfd26534 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -108,9 +108,6 @@ class Settings(object): os.environ['TZ'] = self.TIME_ZONE time.tzset() - def get_all_members(self): - return dir(self) - class UserSettingsHolder(object): """ Holder for user configured settings. @@ -129,8 +126,11 @@ class UserSettingsHolder(object): def __getattr__(self, name): return getattr(self.default_settings, name) - def get_all_members(self): + def __dir__(self): return dir(self) + dir(self.default_settings) + # For Python < 2.6: + __members__ = property(lambda self: self.__dir__()) + settings = LazySettings() diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 99fc72e468..70d9c1e259 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -131,6 +131,12 @@ DATABASE_HOST = '' # Set to empty string for localhost. Not used wit DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. DATABASE_OPTIONS = {} # Set to empty dictionary for default. +# The email backend to use. For possible shortcuts see django.core.mail. +# The default is to use the SMTP backend. +# Third-party backends can be specified by providing a Python path +# to a module that defines an EmailBackend class. +EMAIL_BACKEND = 'django.core.mail.backends.smtp' + # Host for sending e-mail. EMAIL_HOST = 'localhost' @@ -300,6 +306,7 @@ DEFAULT_INDEX_TABLESPACE = '' MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', # 'django.middleware.http.ConditionalGetMiddleware', # 'django.middleware.gzip.GZipMiddleware', @@ -374,6 +381,18 @@ LOGIN_REDIRECT_URL = '/accounts/profile/' # The number of days a password reset link is valid for PASSWORD_RESET_TIMEOUT_DAYS = 3 +######## +# CSRF # +######## + +# Dotted path to callable to be used as view when a request is +# rejected by the CSRF middleware. +CSRF_FAILURE_VIEW = 'django.views.csrf.csrf_failure' + +# Name and domain for CSRF cookie. +CSRF_COOKIE_NAME = 'csrftoken' +CSRF_COOKIE_DOMAIN = None + ########### # TESTING # ########### diff --git a/django/conf/locale/pl/LC_MESSAGES/django.mo b/django/conf/locale/pl/LC_MESSAGES/django.mo index 309db85baf..0106a8c904 100644 Binary files a/django/conf/locale/pl/LC_MESSAGES/django.mo and b/django/conf/locale/pl/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/pl/LC_MESSAGES/django.po b/django/conf/locale/pl/LC_MESSAGES/django.po index ba985d01e9..28f12561ee 100644 --- a/django/conf/locale/pl/LC_MESSAGES/django.po +++ b/django/conf/locale/pl/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-07-17 21:59+0200\n" +"POT-Creation-Date: 2009-10-25 20:56+0100\n" "PO-Revision-Date: 2008-02-25 15:53+0100\n" "Last-Translator: Jarek Zgoda \n" "MIME-Version: 1.0\n" @@ -266,15 +266,15 @@ msgstr "Ten miesiąc" msgid "This year" msgstr "Ten rok" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:434 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:435 msgid "Yes" msgstr "Tak" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:434 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:435 msgid "No" msgstr "Nie" -#: contrib/admin/filterspecs.py:154 forms/widgets.py:434 +#: contrib/admin/filterspecs.py:154 forms/widgets.py:435 msgid "Unknown" msgstr "Nieznany" @@ -320,8 +320,8 @@ msgid "Changed %s." msgstr "Zmieniono %s" #: contrib/admin/options.py:519 contrib/admin/options.py:529 -#: contrib/comments/templates/comments/preview.html:16 forms/models.py:388 -#: forms/models.py:600 +#: contrib/comments/templates/comments/preview.html:16 forms/models.py:384 +#: forms/models.py:596 msgid "and" msgstr "i" @@ -417,11 +417,11 @@ msgstr "" "Proszę wpisać poprawną nazwę użytkownika i hasło. Uwaga: wielkość liter ma " "znaczenie." -#: contrib/admin/sites.py:285 contrib/admin/views/decorators.py:40 +#: contrib/admin/sites.py:288 contrib/admin/views/decorators.py:40 msgid "Please log in again, because your session has expired." msgstr "Twoja sesja wygasła, zaloguj się ponownie." -#: contrib/admin/sites.py:292 contrib/admin/views/decorators.py:47 +#: contrib/admin/sites.py:295 contrib/admin/views/decorators.py:47 msgid "" "Looks like your browser isn't configured to accept cookies. Please enable " "cookies, reload this page, and try again." @@ -429,27 +429,27 @@ msgstr "" "Twoja przeglądarka nie chce akceptować ciasteczek. Zmień jej ustawienia i " "spróbuj ponownie." -#: contrib/admin/sites.py:308 contrib/admin/sites.py:314 +#: contrib/admin/sites.py:311 contrib/admin/sites.py:317 #: contrib/admin/views/decorators.py:66 msgid "Usernames cannot contain the '@' character." msgstr "Nazwy użytkowników nie mogą zawierać znaku '@'." -#: contrib/admin/sites.py:311 contrib/admin/views/decorators.py:62 +#: contrib/admin/sites.py:314 contrib/admin/views/decorators.py:62 #, python-format msgid "Your e-mail address is not your username. Try '%s' instead." msgstr "Podany adres e-mail nie jest Twoją nazwą użytkownika. Spróbuj '%s'." -#: contrib/admin/sites.py:367 +#: contrib/admin/sites.py:370 msgid "Site administration" msgstr "Administracja stroną" -#: contrib/admin/sites.py:381 contrib/admin/templates/admin/login.html:26 +#: contrib/admin/sites.py:384 contrib/admin/templates/admin/login.html:26 #: contrib/admin/templates/registration/password_reset_complete.html:14 #: contrib/admin/views/decorators.py:20 msgid "Log in" msgstr "Zaloguj się" -#: contrib/admin/sites.py:426 +#: contrib/admin/sites.py:429 #, python-format msgid "%s administration" msgstr "%s - administracja" @@ -464,27 +464,27 @@ msgstr "Jedno lub więcej %(fieldname)s w %(name)s: %(obj)s" msgid "One or more %(fieldname)s in %(name)s:" msgstr "Jedno lub więcej %(fieldname)s w %(name)s:" -#: contrib/admin/widgets.py:71 +#: contrib/admin/widgets.py:72 msgid "Date:" msgstr "Data:" -#: contrib/admin/widgets.py:71 +#: contrib/admin/widgets.py:72 msgid "Time:" msgstr "Czas:" -#: contrib/admin/widgets.py:95 +#: contrib/admin/widgets.py:96 msgid "Currently:" msgstr "Teraz:" -#: contrib/admin/widgets.py:95 +#: contrib/admin/widgets.py:96 msgid "Change:" msgstr "Zmień:" -#: contrib/admin/widgets.py:124 +#: contrib/admin/widgets.py:125 msgid "Lookup" msgstr "Szukaj" -#: contrib/admin/widgets.py:235 +#: contrib/admin/widgets.py:237 msgid "Add Another" msgstr "Dodaj kolejny" @@ -598,7 +598,7 @@ msgstr "Historia" #: contrib/admin/templates/admin/change_form.html:28 #: contrib/admin/templates/admin/edit_inline/stacked.html:13 -#: contrib/admin/templates/admin/edit_inline/tabular.html:27 +#: contrib/admin/templates/admin/edit_inline/tabular.html:28 msgid "View on site" msgstr "Pokaż na stronie" @@ -668,10 +668,10 @@ msgstr "" #, python-format msgid "" "Are you sure you want to delete the selected %(object_name)s objects? All of " -"the following objects and it's related items will be deleted:" +"the following objects and their related items will be deleted:" msgstr "" -"Czy chcesz skasować %(object_name)s? Następujące obiekty i zależne od nich " -"zostaną skasowane:" +"Czy chcesz skasować wybrane %(object_name)s? Następujące obiekty i zależne od " +"nich zostaną skasowane:" #: contrib/admin/templates/admin/filter.html:2 #, python-format @@ -734,7 +734,6 @@ msgid "User" msgstr "Użytkownik" #: contrib/admin/templates/admin/object_history.html:24 -#: contrib/comments/templates/comments/moderation_queue.html:33 msgid "Action" msgstr "Akcja" @@ -1125,7 +1124,6 @@ msgid "Time" msgstr "Czas" #: contrib/admindocs/views.py:359 contrib/comments/forms.py:95 -#: contrib/comments/templates/comments/moderation_queue.html:37 #: contrib/flatpages/admin.py:8 contrib/flatpages/models.py:7 msgid "URL" msgstr "URL" @@ -1428,22 +1426,54 @@ msgstr "użytkownicy" msgid "message" msgstr "wiadomość" -#: contrib/auth/views.py:56 +#: contrib/auth/views.py:58 msgid "Logged out" msgstr "Wylogowany" -#: contrib/auth/management/commands/createsuperuser.py:23 forms/fields.py:429 +#: contrib/auth/management/commands/createsuperuser.py:23 forms/fields.py:428 msgid "Enter a valid e-mail address." msgstr "Wprowadź poprawny adres e-mail." -#: contrib/comments/admin.py:11 +#: contrib/comments/admin.py:12 msgid "Content" msgstr "Zawartość" -#: contrib/comments/admin.py:14 +#: contrib/comments/admin.py:15 msgid "Metadata" msgstr "Metadane" +#: contrib/comments/admin.py:39 +msgid "flagged" +msgstr "oflagowany" + +#: contrib/comments/admin.py:40 +msgid "Flag selected comments" +msgstr "Oflaguj wybrane komentarze" + +#: contrib/comments/admin.py:43 +msgid "approved" +msgstr "zaakceptowany" + +#: contrib/comments/admin.py:44 +msgid "Approve selected comments" +msgstr "Zaakceptuj wybrane komentarze" + +#: contrib/comments/admin.py:47 +msgid "removed" +msgstr "usunięty" + +#: contrib/comments/admin.py:48 +msgid "Remove selected comments" +msgstr "Usuń wybrane komentarze" + +#: contrib/comments/admin.py:60 +#, python-format +msgid "1 comment was successfully %(action)s." +msgid_plural "%(count)s comments were successfully %(action)s." +msgstr[0] "1 komentarz został %(action)s" +msgstr[1] "%(count)s komentarze zostały %(action)s" +msgstr[2] "%(count)s komentarzy zostało %(action)s" + #: contrib/comments/feeds.py:13 #, python-format msgid "%(site_name)s comments" @@ -1455,7 +1485,6 @@ msgid "Latest comments on %(site_name)s" msgstr "Ostatnie komentarze na %(site_name)s" #: contrib/comments/forms.py:93 -#: contrib/comments/templates/comments/moderation_queue.html:34 msgid "Name" msgstr "Nazwa" @@ -1464,7 +1493,6 @@ msgid "Email address" msgstr "Adres e-mail" #: contrib/comments/forms.py:96 -#: contrib/comments/templates/comments/moderation_queue.html:35 msgid "Comment" msgstr "Komentarz" @@ -1592,7 +1620,6 @@ msgid "Really make this comment public?" msgstr "Czy ten komentarz na pewno ma być publiczny?" #: contrib/comments/templates/comments/approve.html:12 -#: contrib/comments/templates/comments/moderation_queue.html:49 msgid "Approve" msgstr "Zaakceptuj" @@ -1618,7 +1645,6 @@ msgid "Really remove this comment?" msgstr "Czy na pewno usunąć ten komentarz?" #: contrib/comments/templates/comments/delete.html:12 -#: contrib/comments/templates/comments/moderation_queue.html:53 msgid "Remove" msgstr "Usuń" @@ -1652,39 +1678,6 @@ msgstr "Zapisz" msgid "Preview" msgstr "Podgląd" -#: contrib/comments/templates/comments/moderation_queue.html:4 -#: contrib/comments/templates/comments/moderation_queue.html:19 -msgid "Comment moderation queue" -msgstr "Kolejka moderacji komentarzy" - -#: contrib/comments/templates/comments/moderation_queue.html:26 -msgid "No comments to moderate" -msgstr "Żaden komentarz nie oczekuje na akceptację" - -#: contrib/comments/templates/comments/moderation_queue.html:36 -msgid "Email" -msgstr "E-mail" - -#: contrib/comments/templates/comments/moderation_queue.html:38 -msgid "Authenticated?" -msgstr "Zalogowany?" - -#: contrib/comments/templates/comments/moderation_queue.html:39 -msgid "IP Address" -msgstr "Adres IP" - -#: contrib/comments/templates/comments/moderation_queue.html:40 -msgid "Date posted" -msgstr "Data dodania" - -#: contrib/comments/templates/comments/moderation_queue.html:63 -msgid "yes" -msgstr "tak" - -#: contrib/comments/templates/comments/moderation_queue.html:63 -msgid "no" -msgstr "nie" - #: contrib/comments/templates/comments/posted.html:4 msgid "Thanks for commenting" msgstr "Dziękujemy za dodanie komentarza" @@ -2599,6 +2592,10 @@ msgstr "Niepoprawna suma kontrolna numeru konta bankowego." msgid "Enter a valid Finnish social security number." msgstr "Wpis poprawny numer fińskiego ubezpieczenia socjalnego." +#: contrib/localflavor/fr/forms.py:30 +msgid "Phone numbers must be in 0X XX XX XX XX format." +msgstr "Numery telefoniczne muszą być w formacie 0X XX XX XX XX." + #: contrib/localflavor/in_/forms.py:14 msgid "Enter a zip code in the format XXXXXXX." msgstr "Wpisz kod pocztowy w formacie XXXXXXX." @@ -3944,86 +3941,86 @@ msgstr[2] "" "Proszę podać poprawne identyfikatory %(self)s. Wartości %(value)r są " "niepoprawne." -#: forms/fields.py:54 +#: forms/fields.py:53 msgid "This field is required." msgstr "To pole jest wymagane." -#: forms/fields.py:55 +#: forms/fields.py:54 msgid "Enter a valid value." msgstr "Wpisz poprawną wartość." -#: forms/fields.py:138 +#: forms/fields.py:137 #, python-format msgid "Ensure this value has at most %(max)d characters (it has %(length)d)." msgstr "" "Upewnij się, że ta wartość ma co najwyżej %(max)d znaków (ma długość %" "(length)d)." -#: forms/fields.py:139 +#: forms/fields.py:138 #, python-format msgid "Ensure this value has at least %(min)d characters (it has %(length)d)." msgstr "" "Upewnij się, że ta wartość ma co najmniej %(min)d znaków (ma długość %" "(length)d)." -#: forms/fields.py:166 +#: forms/fields.py:165 msgid "Enter a whole number." msgstr "Wpisz liczbę całkowitą." -#: forms/fields.py:167 forms/fields.py:196 forms/fields.py:225 +#: forms/fields.py:166 forms/fields.py:195 forms/fields.py:224 #, python-format msgid "Ensure this value is less than or equal to %s." msgstr "Upewnij się, że ta wartość jest mniejsza lub równa %s." -#: forms/fields.py:168 forms/fields.py:197 forms/fields.py:226 +#: forms/fields.py:167 forms/fields.py:196 forms/fields.py:225 #, python-format msgid "Ensure this value is greater than or equal to %s." msgstr "Upewnij się, że ta wartość jest większa lub równa %s." -#: forms/fields.py:195 forms/fields.py:224 +#: forms/fields.py:194 forms/fields.py:223 msgid "Enter a number." msgstr "Wpisz liczbę." -#: forms/fields.py:227 +#: forms/fields.py:226 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Upewnij się, że jest nie więcej niż %s cyfr." -#: forms/fields.py:228 +#: forms/fields.py:227 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Upewnij się, że jest nie więcej niż %s miejsc po przecinku." -#: forms/fields.py:229 +#: forms/fields.py:228 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Upewnij się, że jest nie więcej niż %s miejsc przed przecinkiem." -#: forms/fields.py:288 forms/fields.py:863 +#: forms/fields.py:287 forms/fields.py:862 msgid "Enter a valid date." msgstr "Wpisz poprawną datę." -#: forms/fields.py:322 forms/fields.py:864 +#: forms/fields.py:321 forms/fields.py:863 msgid "Enter a valid time." msgstr "Wpisz poprawną godzinę." -#: forms/fields.py:361 +#: forms/fields.py:360 msgid "Enter a valid date/time." msgstr "Wpisz poprawną datę/godzinę." -#: forms/fields.py:447 +#: forms/fields.py:446 msgid "No file was submitted. Check the encoding type on the form." msgstr "Nie wysłano żadnego pliku. Sprawdź typ kodowania formularza." -#: forms/fields.py:448 +#: forms/fields.py:447 msgid "No file was submitted." msgstr "Żaden plik nie został przesłany." -#: forms/fields.py:449 +#: forms/fields.py:448 msgid "The submitted file is empty." msgstr "Wysłany plik jest pusty." -#: forms/fields.py:450 +#: forms/fields.py:449 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -4031,7 +4028,7 @@ msgstr "" "Upewnij się, że nazwa tego pliku ma co najwyżej %(max)d znaków (ma długość %" "(length)d)." -#: forms/fields.py:483 +#: forms/fields.py:482 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -4039,29 +4036,29 @@ msgstr "" "Wgraj poprawny plik graficzny. Ten, który został wgrany, nie jest obrazem, " "albo jest uszkodzony." -#: forms/fields.py:544 +#: forms/fields.py:543 msgid "Enter a valid URL." msgstr "Wpisz poprawny URL." -#: forms/fields.py:545 +#: forms/fields.py:544 msgid "This URL appears to be a broken link." msgstr "Ten odnośnik jest nieprawidłowy." -#: forms/fields.py:625 forms/fields.py:703 +#: forms/fields.py:624 forms/fields.py:702 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Wybierz poprawną wartość. %(value)s nie jest jednym z dostępnych wyborów." -#: forms/fields.py:704 forms/fields.py:765 forms/models.py:1003 +#: forms/fields.py:703 forms/fields.py:764 forms/models.py:999 msgid "Enter a list of values." msgstr "Podaj listę wartości." -#: forms/fields.py:892 +#: forms/fields.py:891 msgid "Enter a valid IPv4 address." msgstr "Wprowadź poprawny adres IPv4." -#: forms/fields.py:902 +#: forms/fields.py:901 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "To pole może zawierać jedynie litery, cyfry, podkreślenia i myślniki." @@ -4070,29 +4067,29 @@ msgstr "To pole może zawierać jedynie litery, cyfry, podkreślenia i myślniki msgid "Order" msgstr "Porządek" -#: forms/models.py:367 +#: forms/models.py:363 #, python-format msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgstr "" "Wartości w %(field_name)s muszą być unikalne dla wyszukiwań %(lookup)s w %" "(date_field)s" -#: forms/models.py:381 forms/models.py:389 +#: forms/models.py:377 forms/models.py:385 #, python-format msgid "%(model_name)s with this %(field_label)s already exists." msgstr "%(field_label)s już istnieje w %(model_name)s." -#: forms/models.py:594 +#: forms/models.py:590 #, python-format msgid "Please correct the duplicate data for %(field)s." msgstr "Popraw zduplikowane dane w %(field)s." -#: forms/models.py:598 +#: forms/models.py:594 #, python-format msgid "Please correct the duplicate data for %(field)s, which must be unique." msgstr "Popraw zduplikowane dane w %(field)s, które wymaga unikalności." -#: forms/models.py:604 +#: forms/models.py:600 #, python-format msgid "" "Please correct the duplicate data for %(field_name)s which must be unique " @@ -4101,24 +4098,24 @@ msgstr "" "Popraw zduplikowane dane w %(field_name)s, które wymaga unikalności dla %" "(lookup)s w polu %(date_field)s." -#: forms/models.py:612 +#: forms/models.py:608 msgid "Please correct the duplicate values below." msgstr "Popraw poniższe zduplikowane wartości." -#: forms/models.py:867 +#: forms/models.py:863 msgid "The inline foreign key did not match the parent instance primary key." msgstr "Osadzony klucz obcy nie pasuje do klucza głównego obiektu rodzica." -#: forms/models.py:930 +#: forms/models.py:926 msgid "Select a valid choice. That choice is not one of the available choices." msgstr "Wybierz poprawną wartość. Podana nie jest jednym z dostępnych wyborów." -#: forms/models.py:1004 +#: forms/models.py:1000 #, python-format msgid "Select a valid choice. %s is not one of the available choices." msgstr "Wybierz poprawną wartość. %s nie jest jednym z dostępnych wyborów." -#: forms/models.py:1006 +#: forms/models.py:1002 #, python-format msgid "\"%s\" is not a valid value for a primary key." msgstr "\"%s\" nie jest poprawną wartością klucza głównego." @@ -4444,3 +4441,27 @@ msgstr "%(verbose_name)s zostało pomyślnie zmienione." #, python-format msgid "The %(verbose_name)s was deleted." msgstr "%(verbose_name)s zostało usunięte." + +#~ msgid "Comment moderation queue" +#~ msgstr "Kolejka moderacji komentarzy" + +#~ msgid "No comments to moderate" +#~ msgstr "Żaden komentarz nie oczekuje na akceptację" + +#~ msgid "Email" +#~ msgstr "E-mail" + +#~ msgid "Authenticated?" +#~ msgstr "Zalogowany?" + +#~ msgid "IP Address" +#~ msgstr "Adres IP" + +#~ msgid "Date posted" +#~ msgstr "Data dodania" + +#~ msgid "yes" +#~ msgstr "tak" + +#~ msgid "no" +#~ msgstr "nie" diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index bbf005ddea..9b0b516c80 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -60,6 +60,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ) diff --git a/django/contrib/admin/media/css/changelists.css b/django/contrib/admin/media/css/changelists.css index 5eb66b4d36..43033780ac 100644 --- a/django/contrib/admin/media/css/changelists.css +++ b/django/contrib/admin/media/css/changelists.css @@ -53,7 +53,7 @@ vertical-align: middle; } -#changelist table thead th:first-child { +#changelist table thead th.action-checkbox-column { width: 1.5em; text-align: center; } diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 45350e8cbb..ff716579c0 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets from django.contrib.admin import helpers from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict +from django.views.decorators.csrf import csrf_protect from django.core.exceptions import PermissionDenied from django.db import models, transaction from django.db.models.fields import BLANK_CHOICE_DASH @@ -152,8 +153,9 @@ class BaseModelAdmin(object): """ Get a form Field for a ManyToManyField. """ - # If it uses an intermediary model, don't show field in admin. - if db_field.rel.through is not None: + # If it uses an intermediary model that isn't auto created, don't show + # a field in admin. + if not db_field.rel.through._meta.auto_created: return None if db_field.name in self.raw_id_fields: @@ -701,6 +703,8 @@ class ModelAdmin(BaseModelAdmin): else: return HttpResponseRedirect(".") + @csrf_protect + @transaction.commit_on_success def add_view(self, request, form_url='', extra_context=None): "The 'add' admin view for this model." model = self.model @@ -786,8 +790,9 @@ class ModelAdmin(BaseModelAdmin): } context.update(extra_context or {}) return self.render_change_form(request, context, form_url=form_url, add=True) - add_view = transaction.commit_on_success(add_view) + @csrf_protect + @transaction.commit_on_success def change_view(self, request, object_id, extra_context=None): "The 'change' admin view for this model." model = self.model @@ -875,8 +880,8 @@ class ModelAdmin(BaseModelAdmin): } context.update(extra_context or {}) return self.render_change_form(request, context, change=True, obj=obj) - change_view = transaction.commit_on_success(change_view) + @csrf_protect def changelist_view(self, request, extra_context=None): "The 'change list' admin view for this model." from django.contrib.admin.views.main import ChangeList, ERROR_FLAG @@ -989,6 +994,7 @@ class ModelAdmin(BaseModelAdmin): 'admin/change_list.html' ], context, context_instance=context_instance) + @csrf_protect def delete_view(self, request, object_id, extra_context=None): "The 'delete' admin view for this model." opts = self.model._meta diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 5f397ecb01..52ef57370d 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -3,6 +3,7 @@ from django import http, template from django.contrib.admin import ModelAdmin from django.contrib.admin import actions from django.contrib.auth import authenticate, login +from django.views.decorators.csrf import csrf_protect from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse @@ -186,11 +187,17 @@ class AdminSite(object): return view(request, *args, **kwargs) if not cacheable: inner = never_cache(inner) + # We add csrf_protect here so this function can be used as a utility + # function for any view, without having to repeat 'csrf_protect'. + inner = csrf_protect(inner) return update_wrapper(inner, view) def get_urls(self): from django.conf.urls.defaults import patterns, url, include + if settings.DEBUG: + self.check_dependencies() + def wrap(view, cacheable=False): def wrapper(*args, **kwargs): return self.admin_view(view, cacheable)(*args, **kwargs) diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index d28dd0f45c..11414d1465 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -15,7 +15,7 @@ {% endif %}{% endblock %} {% block content %}
-
{% block form_top %}{% endblock %} +{% csrf_token %}{% block form_top %}{% endblock %}
{% if is_popup %}{% endif %} {% if form.errors %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index f645d65a0f..c5ac729c7e 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -29,7 +29,7 @@ {% endif %}{% endif %} {% endblock %} -{% block form_top %}{% endblock %} +{% csrf_token %}{% block form_top %}{% endblock %}
{% if is_popup %}{% endif %} {% if save_on_top %}{% submit_row %}{% endif %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 31bf7bd29a..20b2eff060 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -68,7 +68,7 @@ {% endif %} {% endblock %} - + {% csrf_token %} {% if cl.formset %} {{ cl.formset.management_form }} {% endif %} diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html index 42802f57bc..65e73c922d 100644 --- a/django/contrib/admin/templates/admin/delete_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_confirmation.html @@ -22,7 +22,7 @@ {% else %}

{% 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 %}

    {{ deleted_objects|unordered_list }}
- + {% csrf_token %}
diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html index 5550b73e2e..7f4fbc5726 100644 --- a/django/contrib/admin/templates/admin/delete_selected_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html @@ -23,7 +23,7 @@ {% for deleteable_object in deletable_objects %}
    {{ deleteable_object|unordered_list }}
{% endfor %} - + {% csrf_token %}
{% for obj in queryset %} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index d162e5a9fa..876c4b0327 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -14,7 +14,7 @@

{{ error_message }}

{% endif %}
- +{% csrf_token %}
diff --git a/django/contrib/admin/templates/admin/template_validator.html b/django/contrib/admin/templates/admin/template_validator.html index d221807486..9a139c5d49 100644 --- a/django/contrib/admin/templates/admin/template_validator.html +++ b/django/contrib/admin/templates/admin/template_validator.html @@ -4,7 +4,7 @@
- +{% csrf_token %} {% if form.errors %}

Your template had {{ form.errors|length }} error{{ form.errors|pluralize }}:

diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index c13c7f7040..6d7a6609de 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -11,7 +11,7 @@

{% 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." %}

- +{% csrf_token %} {{ form.old_password.errors }}

{{ form.old_password }}

diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html index 049ee625a9..df9cf1b316 100644 --- a/django/contrib/admin/templates/registration/password_reset_confirm.html +++ b/django/contrib/admin/templates/registration/password_reset_confirm.html @@ -13,7 +13,7 @@

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

- +{% csrf_token %} {{ form.new_password1.errors }}

{{ form.new_password1 }}

{{ form.new_password2.errors }} diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index 704066c68a..d3a128428a 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -11,7 +11,7 @@

{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}

- +{% csrf_token %} {{ form.email.errors }}

{{ form.email }}

diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 9a4ce3b266..5a02ab01be 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -106,6 +106,11 @@ def result_headers(cl): else: header = field_name header = header.replace('_', ' ') + # if the field is the action checkbox: no sorting and special class + if field_name == 'action_checkbox': + yield {"text": header, + "class_attrib": mark_safe(' class="action-checkbox-column"')} + continue # It is a non-field, but perhaps one that is sortable admin_order_field = getattr(attr, "admin_order_field", None) diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 50e41437b8..05e5c6d300 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -149,12 +149,16 @@ def validate(cls, model): validate_inline(inline, cls, model) def validate_inline(cls, parent, parent_model): + # model is already verified to exist and be a Model if cls.fk_name: # default value is None f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name) if not isinstance(f, models.ForeignKey): raise ImproperlyConfigured("'%s.fk_name is not an instance of " "models.ForeignKey." % cls.__name__) + + fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) + # extra = 3 # max_num = 0 for attr in ('extra', 'max_num'): @@ -169,7 +173,6 @@ def validate_inline(cls, parent, parent_model): # exclude if hasattr(cls, 'exclude') and cls.exclude: - fk = _get_foreign_key(parent_model, cls.model, can_fail=True) if fk and fk.name in cls.exclude: raise ImproperlyConfigured("%s cannot exclude the field " "'%s' - this is the foreign key to the parent model " diff --git a/django/contrib/auth/tests/remote_user.py b/django/contrib/auth/tests/remote_user.py index 842d589a54..6115edcfd0 100644 --- a/django/contrib/auth/tests/remote_user.py +++ b/django/contrib/auth/tests/remote_user.py @@ -2,7 +2,7 @@ from datetime import datetime from django.conf import settings from django.contrib.auth.backends import RemoteUserBackend -from django.contrib.auth.models import AnonymousUser, User +from django.contrib.auth.models import User from django.test import TestCase @@ -30,15 +30,15 @@ class RemoteUserTest(TestCase): num_users = User.objects.count() response = self.client.get('/remote_user/') - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) response = self.client.get('/remote_user/', REMOTE_USER=None) - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) response = self.client.get('/remote_user/', REMOTE_USER='') - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) def test_unknown_user(self): @@ -115,7 +115,7 @@ class RemoteUserNoCreateTest(RemoteUserTest): def test_unknown_user(self): num_users = User.objects.count() response = self.client.get('/remote_user/', REMOTE_USER='newuser') - self.assert_(isinstance(response.context['user'], AnonymousUser)) + self.assert_(response.context['user'].is_anonymous()) self.assertEqual(User.objects.count(), num_users) diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index f753ed6de8..d427874df0 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -4,6 +4,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm from django.contrib.auth.tokens import default_token_generator +from django.views.decorators.csrf import csrf_protect from django.core.urlresolvers import reverse from django.shortcuts import render_to_response, get_object_or_404 from django.contrib.sites.models import Site, RequestSite @@ -14,11 +15,15 @@ from django.utils.translation import ugettext as _ from django.contrib.auth.models import User from django.views.decorators.cache import never_cache -def login(request, template_name='registration/login.html', redirect_field_name=REDIRECT_FIELD_NAME): +@csrf_protect +@never_cache +def login(request, template_name='registration/login.html', + redirect_field_name=REDIRECT_FIELD_NAME, + authentication_form=AuthenticationForm): "Displays the login form and handles the login action." redirect_to = request.REQUEST.get(redirect_field_name, '') if request.method == "POST": - form = AuthenticationForm(data=request.POST) + form = authentication_form(data=request.POST) if form.is_valid(): # Light security check -- make sure redirect_to isn't garbage. if not redirect_to or '//' in redirect_to or ' ' in redirect_to: @@ -29,7 +34,7 @@ def login(request, template_name='registration/login.html', redirect_field_name= request.session.delete_test_cookie() return HttpResponseRedirect(redirect_to) else: - form = AuthenticationForm(request) + form = authentication_form(request) request.session.set_test_cookie() if Site._meta.installed: current_site = Site.objects.get_current() @@ -41,7 +46,6 @@ def login(request, template_name='registration/login.html', redirect_field_name= 'site': current_site, 'site_name': current_site.name, }, context_instance=RequestContext(request)) -login = never_cache(login) def logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name=REDIRECT_FIELD_NAME): "Logs out the user and displays 'You are logged out' message." @@ -78,6 +82,7 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N # prompts for a new password # - password_reset_complete shows a success message for the above +@csrf_protect def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html', password_reset_form=PasswordResetForm, token_generator=default_token_generator, @@ -107,6 +112,7 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas def password_reset_done(request, template_name='registration/password_reset_done.html'): return render_to_response(template_name, context_instance=RequestContext(request)) +# Doesn't need csrf_protect since no-one can guess the URL def password_reset_confirm(request, uidb36=None, token=None, template_name='registration/password_reset_confirm.html', token_generator=default_token_generator, set_password_form=SetPasswordForm, post_reset_redirect=None): @@ -137,28 +143,29 @@ def password_reset_confirm(request, uidb36=None, token=None, template_name='regi else: context_instance['validlink'] = False form = None - context_instance['form'] = form + context_instance['form'] = form return render_to_response(template_name, context_instance=context_instance) def password_reset_complete(request, template_name='registration/password_reset_complete.html'): return render_to_response(template_name, context_instance=RequestContext(request, {'login_url': settings.LOGIN_URL})) +@csrf_protect +@login_required def password_change(request, template_name='registration/password_change_form.html', - post_change_redirect=None): + post_change_redirect=None, password_change_form=PasswordChangeForm): if post_change_redirect is None: post_change_redirect = reverse('django.contrib.auth.views.password_change_done') if request.method == "POST": - form = PasswordChangeForm(request.user, request.POST) + form = password_change_form(user=request.user, data=request.POST) if form.is_valid(): form.save() return HttpResponseRedirect(post_change_redirect) else: - form = PasswordChangeForm(request.user) + form = password_change_form(user=request.user) return render_to_response(template_name, { 'form': form, }, context_instance=RequestContext(request)) -password_change = login_required(password_change) def password_change_done(request, template_name='registration/password_change_done.html'): return render_to_response(template_name, context_instance=RequestContext(request)) diff --git a/django/contrib/comments/admin.py b/django/contrib/comments/admin.py index c2f8e564f4..ede833f530 100644 --- a/django/contrib/comments/admin.py +++ b/django/contrib/comments/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin from django.contrib.comments.models import Comment -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext_lazy as _, ungettext from django.contrib.comments import get_model +from django.contrib.comments.views.moderation import perform_flag, perform_approve, perform_delete class CommentsAdmin(admin.ModelAdmin): fieldsets = ( @@ -22,6 +23,44 @@ class CommentsAdmin(admin.ModelAdmin): ordering = ('-submit_date',) raw_id_fields = ('user',) search_fields = ('comment', 'user__username', 'user_name', 'user_email', 'user_url', 'ip_address') + actions = ["flag_comments", "approve_comments", "remove_comments"] + + def get_actions(self, request): + actions = super(CommentsAdmin, self).get_actions(request) + # Only superusers should be able to delete the comments from the DB. + if not request.user.is_superuser: + actions.pop('delete_selected') + if not request.user.has_perm('comments.can_moderate'): + actions.pop('approve_comments') + actions.pop('remove_comments') + return actions + + def flag_comments(self, request, queryset): + self._bulk_flag(request, queryset, perform_flag, _("flagged")) + flag_comments.short_description = _("Flag selected comments") + + def approve_comments(self, request, queryset): + self._bulk_flag(request, queryset, perform_approve, _('approved')) + approve_comments.short_description = _("Approve selected comments") + + def remove_comments(self, request, queryset): + self._bulk_flag(request, queryset, perform_delete, _('removed')) + remove_comments.short_description = _("Remove selected comments") + + def _bulk_flag(self, request, queryset, action, description): + """ + Flag, approve, or remove some comments from an admin action. Actually + calls the `action` argument to perform the heavy lifting. + """ + n_comments = 0 + for comment in queryset: + action(request, comment) + n_comments += 1 + + msg = ungettext(u'1 comment was successfully %(action)s.', + u'%(count)s comments were successfully %(action)s.', + n_comments) + self.message_user(request, msg % {'count': n_comments, 'action': description}) # Only register the default admin if the model is the built-in comment model # (this won't be true if there's a custom comment app). diff --git a/django/contrib/comments/templates/comments/approve.html b/django/contrib/comments/templates/comments/approve.html index a4306a6fc2..1a3a3fd80c 100644 --- a/django/contrib/comments/templates/comments/approve.html +++ b/django/contrib/comments/templates/comments/approve.html @@ -6,7 +6,7 @@ {% block content %}

{% trans "Really make this comment public?" %}

{{ comment|linebreaks }}
-
+ {% csrf_token %} {% if next %}{% endif %}

or cancel diff --git a/django/contrib/comments/templates/comments/delete.html b/django/contrib/comments/templates/comments/delete.html index 7d73eac979..5ff2add9c5 100644 --- a/django/contrib/comments/templates/comments/delete.html +++ b/django/contrib/comments/templates/comments/delete.html @@ -6,7 +6,7 @@ {% block content %}

{% trans "Really remove this comment?" %}

{{ comment|linebreaks }}
- + {% csrf_token %} {% if next %}{% endif %}

or cancel diff --git a/django/contrib/comments/templates/comments/flag.html b/django/contrib/comments/templates/comments/flag.html index 08dbe0b0b0..0b9ab1ccb2 100644 --- a/django/contrib/comments/templates/comments/flag.html +++ b/django/contrib/comments/templates/comments/flag.html @@ -6,7 +6,7 @@ {% block content %}

{% trans "Really flag this comment?" %}

{{ comment|linebreaks }}
- + {% csrf_token %} {% if next %}{% endif %}

or cancel diff --git a/django/contrib/comments/templates/comments/form.html b/django/contrib/comments/templates/comments/form.html index d8e248372f..30f031128c 100644 --- a/django/contrib/comments/templates/comments/form.html +++ b/django/contrib/comments/templates/comments/form.html @@ -1,5 +1,5 @@ {% load comments i18n %} - +{% csrf_token %} {% if next %}{% endif %} {% for field in form %} {% if field.is_hidden %} diff --git a/django/contrib/comments/templates/comments/moderation_queue.html b/django/contrib/comments/templates/comments/moderation_queue.html deleted file mode 100644 index 73012b3539..0000000000 --- a/django/contrib/comments/templates/comments/moderation_queue.html +++ /dev/null @@ -1,75 +0,0 @@ -{% extends "admin/change_list.html" %} -{% load adminmedia i18n %} - -{% block title %}{% trans "Comment moderation queue" %}{% endblock %} - -{% block extrahead %} - {{ block.super }} - -{% endblock %} - -{% block branding %} -

{% trans "Comment moderation queue" %}

-{% endblock %} - -{% block breadcrumbs %}{% endblock %} - -{% block content %} -{% if empty %} -

{% trans "No comments to moderate" %}.

-{% else %} -
-
- - - - - - - - - - - - - - - {% for comment in comments %} - - - - - - - - - - - {% endfor %} - -
{% trans "Action" %}{% trans "Name" %}{% trans "Comment" %}{% trans "Email" %}{% trans "URL" %}{% trans "Authenticated?" %}{% trans "IP Address" %}{% trans "Date posted" %}
- - - - -
- - -
-
{{ comment.name }}{{ comment.comment|truncatewords:"50" }}{{ comment.email }}{{ comment.url }} - {% if comment.user %}{% trans - {{ comment.ip_address }}{{ comment.submit_date|date:"F j, P" }}
-
-
-{% endif %} -{% endblock %} diff --git a/django/contrib/comments/templates/comments/preview.html b/django/contrib/comments/templates/comments/preview.html index d3884575f5..1b072a76f0 100644 --- a/django/contrib/comments/templates/comments/preview.html +++ b/django/contrib/comments/templates/comments/preview.html @@ -5,7 +5,7 @@ {% block content %} {% load comments %} -
+ {% csrf_token %} {% if next %}{% endif %} {% if form.errors %}

{% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}

diff --git a/django/contrib/comments/urls.py b/django/contrib/comments/urls.py index 5caef9c7d4..2bfefa3e2d 100644 --- a/django/contrib/comments/urls.py +++ b/django/contrib/comments/urls.py @@ -7,7 +7,6 @@ urlpatterns = patterns('django.contrib.comments.views', url(r'^flagged/$', 'moderation.flag_done', name='comments-flag-done'), url(r'^delete/(\d+)/$', 'moderation.delete', name='comments-delete'), url(r'^deleted/$', 'moderation.delete_done', name='comments-delete-done'), - url(r'^moderate/$', 'moderation.moderation_queue', name='comments-moderation-queue'), url(r'^approve/(\d+)/$', 'moderation.approve', name='comments-approve'), url(r'^approved/$', 'moderation.approve_done', name='comments-approve-done'), ) diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 89a3dd9bba..7fbe80eead 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -10,6 +10,7 @@ from django.utils.html import escape from django.views.decorators.http import require_POST from django.contrib import comments from django.contrib.comments import signals +from django.views.decorators.csrf import csrf_protect class CommentPostBadRequest(http.HttpResponseBadRequest): """ @@ -22,6 +23,8 @@ class CommentPostBadRequest(http.HttpResponseBadRequest): if settings.DEBUG: self.content = render_to_string("comments/400-debug.html", {"why": why}) +@csrf_protect +@require_POST def post_comment(request, next=None): """ Post a comment. @@ -116,8 +119,6 @@ def post_comment(request, next=None): return next_redirect(data, next, comment_done, c=comment._get_pk_val()) -post_comment = require_POST(post_comment) - comment_done = confirmation_view( template = "comments/posted.html", doc = """Display a "comment was posted" success page.""" diff --git a/django/contrib/comments/views/moderation.py b/django/contrib/comments/views/moderation.py index 3334b0927e..73304ba416 100644 --- a/django/contrib/comments/views/moderation.py +++ b/django/contrib/comments/views/moderation.py @@ -3,12 +3,12 @@ from django.conf import settings from django.shortcuts import get_object_or_404, render_to_response from django.contrib.auth.decorators import login_required, permission_required from utils import next_redirect, confirmation_view -from django.core.paginator import Paginator, InvalidPage -from django.http import Http404 from django.contrib import comments from django.contrib.comments import signals +from django.views.decorators.csrf import csrf_protect -#@login_required +@csrf_protect +@login_required def flag(request, comment_id, next=None): """ Flags a comment. Confirmation on GET, action on POST. @@ -22,18 +22,7 @@ def flag(request, comment_id, next=None): # Flag on POST if request.method == 'POST': - flag, created = comments.models.CommentFlag.objects.get_or_create( - comment = comment, - user = request.user, - flag = comments.models.CommentFlag.SUGGEST_REMOVAL - ) - signals.comment_was_flagged.send( - sender = comment.__class__, - comment = comment, - flag = flag, - created = created, - request = request, - ) + perform_flag(request, comment) return next_redirect(request.POST.copy(), next, flag_done, c=comment.pk) # Render a form on GET @@ -42,9 +31,9 @@ def flag(request, comment_id, next=None): {'comment': comment, "next": next}, template.RequestContext(request) ) -flag = login_required(flag) -#@permission_required("comments.delete_comment") +@csrf_protect +@permission_required("comments.can_moderate") def delete(request, comment_id, next=None): """ Deletes a comment. Confirmation on GET, action on POST. Requires the "can @@ -60,20 +49,7 @@ def delete(request, comment_id, next=None): # Delete on POST if request.method == 'POST': # Flag the comment as deleted instead of actually deleting it. - flag, created = comments.models.CommentFlag.objects.get_or_create( - comment = comment, - user = request.user, - flag = comments.models.CommentFlag.MODERATOR_DELETION - ) - comment.is_removed = True - comment.save() - signals.comment_was_flagged.send( - sender = comment.__class__, - comment = comment, - flag = flag, - created = created, - request = request, - ) + perform_delete(request, comment) return next_redirect(request.POST.copy(), next, delete_done, c=comment.pk) # Render a form on GET @@ -82,9 +58,9 @@ def delete(request, comment_id, next=None): {'comment': comment, "next": next}, template.RequestContext(request) ) -delete = permission_required("comments.can_moderate")(delete) -#@permission_required("comments.can_moderate") +@csrf_protect +@permission_required("comments.can_moderate") def approve(request, comment_id, next=None): """ Approve a comment (that is, mark it as public and non-removed). Confirmation @@ -100,23 +76,7 @@ def approve(request, comment_id, next=None): # Delete on POST if request.method == 'POST': # Flag the comment as approved. - flag, created = comments.models.CommentFlag.objects.get_or_create( - comment = comment, - user = request.user, - flag = comments.models.CommentFlag.MODERATOR_APPROVAL, - ) - - comment.is_removed = False - comment.is_public = True - comment.save() - - signals.comment_was_flagged.send( - sender = comment.__class__, - comment = comment, - flag = flag, - created = created, - request = request, - ) + perform_approve(request, comment) return next_redirect(request.POST.copy(), next, approve_done, c=comment.pk) # Render a form on GET @@ -126,69 +86,64 @@ def approve(request, comment_id, next=None): template.RequestContext(request) ) -approve = permission_required("comments.can_moderate")(approve) +# The following functions actually perform the various flag/aprove/delete +# actions. They've been broken out into seperate functions to that they +# may be called from admin actions. - -#@permission_required("comments.can_moderate") -def moderation_queue(request): +def perform_flag(request, comment): """ - Displays a list of unapproved comments to be approved. - - 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", diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 40a96e534c..27d44cc5e5 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -105,8 +105,6 @@ class GenericRelation(RelatedField, Field): limit_choices_to=kwargs.pop('limit_choices_to', None), symmetrical=kwargs.pop('symmetrical', True)) - # By its very nature, a GenericRelation doesn't create a table. - self.creates_table = False # Override content-type/object-id field names on the related class self.object_id_field_name = kwargs.pop("object_id_field", "object_id") diff --git a/django/contrib/csrf/middleware.py b/django/contrib/csrf/middleware.py index 0d0a8eca9e..4885cfcc3e 100644 --- a/django/contrib/csrf/middleware.py +++ b/django/contrib/csrf/middleware.py @@ -1,160 +1,7 @@ -""" -Cross Site Request Forgery Middleware. +from django.middleware.csrf import CsrfMiddleware, CsrfViewMiddleware, CsrfResponseMiddleware +from django.views.decorators.csrf import csrf_exempt, csrf_view_exempt, csrf_response_exempt -This module provides a middleware that implements protection -against request forgeries from other sites. -""" - -import re -import itertools -try: - from functools import wraps -except ImportError: - from django.utils.functional import wraps # Python 2.3, 2.4 fallback. - -from django.conf import settings -from django.http import HttpResponseForbidden -from django.utils.hashcompat import md5_constructor -from django.utils.safestring import mark_safe - -_ERROR_MSG = mark_safe('

403 Forbidden

Cross Site Request Forgery detected. Request aborted.

') - -_POST_FORM_RE = \ - re.compile(r'(]*\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 tag plus the added element""" - return mark_safe(match.group() + "
" + \ - "
") - - # Modify any POST forms - response.content = _POST_FORM_RE.sub(add_csrf_field, response.content) - return response - -class CsrfMiddleware(CsrfViewMiddleware, CsrfResponseMiddleware): - """Django middleware that adds protection against Cross Site - Request Forgeries by adding hidden form fields to POST forms and - checking requests for the correct value. - - In the list of middlewares, SessionMiddleware is required, and - must come after this middleware. CsrfMiddleWare must come after - compression middleware. - - If a session ID cookie is present, it is hashed with the - SECRET_KEY setting to create an authentication token. This token - is added to all outgoing POST forms and is expected on all - incoming POST requests that have a session ID cookie. - - If you are setting cookies directly, instead of using Django's - session framework, this middleware will not work. - - CsrfMiddleWare is composed of two middleware, CsrfViewMiddleware - and CsrfResponseMiddleware which can be used independently. - """ - pass - -def csrf_response_exempt(view_func): - """ - Modifies a view function so that its response is exempt - from the post-processing of the CSRF middleware. - """ - def wrapped_view(*args, **kwargs): - resp = view_func(*args, **kwargs) - resp.csrf_exempt = True - return resp - return wraps(view_func)(wrapped_view) - -def csrf_view_exempt(view_func): - """ - Marks a view function as being exempt from CSRF view protection. - """ - # We could just do view_func.csrf_exempt = True, but decorators - # are nicer if they don't have side-effects, so we return a new - # function. - def wrapped_view(*args, **kwargs): - return view_func(*args, **kwargs) - wrapped_view.csrf_exempt = True - return wraps(view_func)(wrapped_view) - -def csrf_exempt(view_func): - """ - Marks a view function as being exempt from the CSRF checks - and post processing. - - This is the same as using both the csrf_view_exempt and - csrf_response_exempt decorators. - """ - return csrf_response_exempt(csrf_view_exempt(view_func)) +import warnings +warnings.warn("This import for CSRF functionality is deprecated. Please use django.middleware.csrf for the middleware and django.views.decorators.csrf for decorators.", + PendingDeprecationWarning + ) diff --git a/django/contrib/csrf/tests.py b/django/contrib/csrf/tests.py deleted file mode 100644 index 3c533a01e6..0000000000 --- a/django/contrib/csrf/tests.py +++ /dev/null @@ -1,144 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.test import TestCase -from django.http import HttpRequest, HttpResponse, HttpResponseForbidden -from django.contrib.csrf.middleware import CsrfMiddleware, _make_token, csrf_exempt -from django.conf import settings - - -def post_form_response(): - resp = HttpResponse(content=""" - -""", mimetype="text/html") - return resp - -def test_view(request): - return post_form_response() - -class CsrfMiddlewareTest(TestCase): - - _session_id = "1" - - def _get_GET_no_session_request(self): - return HttpRequest() - - def _get_GET_session_request(self): - req = self._get_GET_no_session_request() - req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id - return req - - def _get_POST_session_request(self): - req = self._get_GET_session_request() - req.method = "POST" - return req - - def _get_POST_no_session_request(self): - req = self._get_GET_no_session_request() - req.method = "POST" - return req - - def _get_POST_session_request_with_token(self): - req = self._get_POST_session_request() - req.POST['csrfmiddlewaretoken'] = _make_token(self._session_id) - return req - - def _get_post_form_response(self): - return post_form_response() - - def _get_new_session_response(self): - resp = self._get_post_form_response() - resp.cookies[settings.SESSION_COOKIE_NAME] = self._session_id - return resp - - def _check_token_present(self, response): - self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(self._session_id)) - - def get_view(self): - return test_view - - # Check the post processing - def test_process_response_no_session(self): - """ - Check the post-processor does nothing if no session active - """ - req = self._get_GET_no_session_request() - resp = self._get_post_form_response() - resp_content = resp.content # needed because process_response modifies resp - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertEquals(resp_content, resp2.content) - - def test_process_response_existing_session(self): - """ - Check that the token is inserted if there is an existing session - """ - req = self._get_GET_session_request() - resp = self._get_post_form_response() - resp_content = resp.content # needed because process_response modifies resp - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertNotEqual(resp_content, resp2.content) - self._check_token_present(resp2) - - def test_process_response_new_session(self): - """ - Check that the token is inserted if there is a new session being started - """ - req = self._get_GET_no_session_request() # no session in request - resp = self._get_new_session_response() # but new session started - resp_content = resp.content # needed because process_response modifies resp - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertNotEqual(resp_content, resp2.content) - self._check_token_present(resp2) - - def test_process_response_exempt_view(self): - """ - Check that no post processing is done for an exempt view - """ - req = self._get_POST_session_request() - resp = csrf_exempt(self.get_view())(req) - resp_content = resp.content - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertEquals(resp_content, resp2.content) - - # Check the request processing - def test_process_request_no_session(self): - """ - Check that if no session is present, the middleware does nothing. - to the incoming request. - """ - req = self._get_POST_no_session_request() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(None, req2) - - def test_process_request_session_no_token(self): - """ - Check that if a session is present but no token, we get a 'forbidden' - """ - req = self._get_POST_session_request() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(HttpResponseForbidden, req2.__class__) - - def test_process_request_session_and_token(self): - """ - Check that if a session is present and a token, the middleware lets it through - """ - req = self._get_POST_session_request_with_token() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(None, req2) - - def test_process_request_session_no_token_exempt_view(self): - """ - Check that if a session is present and no token, but the csrf_exempt - decorator has been applied to the view, the middleware lets it through - """ - req = self._get_POST_session_request() - req2 = CsrfMiddleware().process_view(req, csrf_exempt(self.get_view()), (), {}) - self.assertEquals(None, req2) - - def test_ajax_exemption(self): - """ - Check that AJAX requests are automatically exempted. - """ - req = self._get_POST_session_request() - req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(None, req2) diff --git a/django/contrib/formtools/templates/formtools/form.html b/django/contrib/formtools/templates/formtools/form.html index 194bbdd675..2f2de1f637 100644 --- a/django/contrib/formtools/templates/formtools/form.html +++ b/django/contrib/formtools/templates/formtools/form.html @@ -4,7 +4,7 @@ {% if form.errors %}

Please correct the following errors

{% else %}

Submit

{% endif %} -
+{% csrf_token %} {{ form }}
diff --git a/django/contrib/formtools/templates/formtools/preview.html b/django/contrib/formtools/templates/formtools/preview.html index c53ce91724..eb88b1ec2e 100644 --- a/django/contrib/formtools/templates/formtools/preview.html +++ b/django/contrib/formtools/templates/formtools/preview.html @@ -15,7 +15,7 @@

Security hash: {{ hash_value }}

- +{% csrf_token %} {% for field in form %}{{ field.as_hidden }} {% endfor %} @@ -25,7 +25,7 @@

Or edit it again

- +{% csrf_token %} {{ form }}
diff --git a/django/contrib/formtools/tests.py b/django/contrib/formtools/tests.py index 86d40b963b..bc65a60fbe 100644 --- a/django/contrib/formtools/tests.py +++ b/django/contrib/formtools/tests.py @@ -147,15 +147,18 @@ class WizardPageTwoForm(forms.Form): class WizardClass(wizard.FormWizard): def render_template(self, *args, **kw): - return "" + return http.HttpResponse("") def done(self, request, cleaned_data): return http.HttpResponse(success_string) -class DummyRequest(object): +class DummyRequest(http.HttpRequest): def __init__(self, POST=None): + super(DummyRequest, self).__init__() self.method = POST and "POST" or "GET" - self.POST = POST + if POST is not None: + self.POST.update(POST) + self._dont_enforce_csrf_checks = True class WizardTests(TestCase): def test_step_starts_at_zero(self): diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py index b075628c49..4729e155b8 100644 --- a/django/contrib/formtools/wizard.py +++ b/django/contrib/formtools/wizard.py @@ -14,6 +14,7 @@ from django.template.context import RequestContext from django.utils.hashcompat import md5_constructor from django.utils.translation import ugettext_lazy as _ from django.contrib.formtools.utils import security_hash +from django.views.decorators.csrf import csrf_protect class FormWizard(object): # Dictionary of extra template context variables. @@ -44,6 +45,7 @@ class FormWizard(object): # hook methods might alter self.form_list. return len(self.form_list) + @csrf_protect def __call__(self, request, *args, **kwargs): """ Main method that does all the hard work, conforming to the Django view diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index 05b824b95d..91d6a6493d 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -29,7 +29,7 @@ +proj=longlat +ellps=clrk66 +datum=NAD27 +no_defs >>> print mpnt MULTIPOINT (-89.999930378602485 29.999797886557641,-89.999930378602485 29.999797886557641) - + The OGRGeomType class is to make it easy to specify an OGR geometry type: >>> from django.contrib.gis.gdal import OGRGeomType >>> gt1 = OGRGeomType(3) # Using an integer for the type @@ -78,7 +78,7 @@ class OGRGeometry(GDALBase): geom_input = buffer(a2b_hex(geom_input.upper())) str_instance = False - # Constructing the geometry, + # Constructing the geometry, if str_instance: # Checking if unicode if isinstance(geom_input, unicode): @@ -130,12 +130,12 @@ class OGRGeometry(GDALBase): self.__class__ = GEO_CLASSES[self.geom_type.num] @classmethod - def from_bbox(cls, bbox): + def from_bbox(cls, bbox): "Constructs a Polygon from a bounding box (4-tuple)." x0, y0, x1, y1 = bbox return OGRGeometry( 'POLYGON((%s %s, %s %s, %s %s, %s %s, %s %s))' % ( x0, y0, x0, y1, x1, y1, x1, y0, x0, y0) ) - + def __del__(self): "Deletes this Geometry." if self._ptr: capi.destroy_geom(self._ptr) @@ -179,10 +179,17 @@ class OGRGeometry(GDALBase): "Returns 0 for points, 1 for lines, and 2 for surfaces." return capi.get_dims(self.ptr) - @property - def coord_dim(self): + def _get_coord_dim(self): "Returns the coordinate dimension of the Geometry." - return capi.get_coord_dims(self.ptr) + return capi.get_coord_dim(self.ptr) + + def _set_coord_dim(self, dim): + "Sets the coordinate dimension of this Geometry." + if not dim in (2, 3): + raise ValueError('Geometry dimension must be either 2 or 3') + capi.set_coord_dim(self.ptr, dim) + + coord_dim = property(_get_coord_dim, _set_coord_dim) @property def geom_count(self): @@ -237,7 +244,7 @@ class OGRGeometry(GDALBase): return self.envelope.tuple #### SpatialReference-related Properties #### - + # The SRS property def _get_srs(self): "Returns the Spatial Reference for this Geometry." @@ -249,11 +256,15 @@ class OGRGeometry(GDALBase): def _set_srs(self, srs): "Sets the SpatialReference for this geometry." + # Do not have to clone the `SpatialReference` object pointer because + # when it is assigned to this `OGRGeometry` it's internal OGR + # reference count is incremented, and will likewise be released + # (decremented) when this geometry's destructor is called. if isinstance(srs, SpatialReference): - srs_ptr = srs_api.clone_srs(srs.ptr) + srs_ptr = srs.ptr elif isinstance(srs, (int, long, basestring)): sr = SpatialReference(srs) - srs_ptr = srs_api.clone_srs(sr.ptr) + srs_ptr = sr.ptr else: raise TypeError('Cannot assign spatial reference with object of type: %s' % type(srs)) capi.assign_srs(self.ptr, srs_ptr) @@ -298,7 +309,7 @@ class OGRGeometry(GDALBase): Returns the GeoJSON representation of this Geometry (requires GDAL 1.5+). """ - if GEOJSON: + if GEOJSON: return capi.to_json(self.ptr) else: raise NotImplementedError('GeoJSON output only supported on GDAL 1.5+.') @@ -335,7 +346,7 @@ class OGRGeometry(GDALBase): def wkt(self): "Returns the WKT representation of the Geometry." return capi.to_wkt(self.ptr, byref(c_char_p())) - + #### Geometry Methods #### def clone(self): "Clones this OGR Geometry." @@ -363,6 +374,16 @@ class OGRGeometry(GDALBase): klone = self.clone() klone.transform(coord_trans) return klone + + # Have to get the coordinate dimension of the original geometry + # so it can be used to reset the transformed geometry's dimension + # afterwards. This is done because of GDAL bug (in versions prior + # to 1.7) that turns geometries 3D after transformation, see: + # http://trac.osgeo.org/gdal/changeset/17792 + orig_dim = self.coord_dim + + # Depending on the input type, use the appropriate OGR routine + # to perform the transformation. if isinstance(coord_trans, CoordTransform): capi.geom_transform(self.ptr, coord_trans.ptr) elif isinstance(coord_trans, SpatialReference): @@ -373,6 +394,10 @@ class OGRGeometry(GDALBase): else: raise TypeError('Transform only accepts CoordTransform, SpatialReference, string, and integer objects.') + # Setting with original dimension, see comment above. + if self.coord_dim != orig_dim: + self.coord_dim = orig_dim + def transform_to(self, srs): "For backwards-compatibility." self.transform(srs) @@ -391,7 +416,7 @@ class OGRGeometry(GDALBase): def intersects(self, other): "Returns True if this geometry intersects with the other." return self._topology(capi.ogr_intersects, other) - + def equals(self, other): "Returns True if this geometry is equivalent to the other." return self._topology(capi.ogr_equals, other) @@ -436,7 +461,7 @@ class OGRGeometry(GDALBase): @property def convex_hull(self): """ - Returns the smallest convex Polygon that contains all the points in + Returns the smallest convex Polygon that contains all the points in this Geometry. """ return self._geomgen(capi.geom_convex_hull) @@ -456,7 +481,7 @@ class OGRGeometry(GDALBase): return self._geomgen(capi.geom_intersection, other) def sym_difference(self, other): - """ + """ Returns a new geometry which is the symmetric difference of this geometry and the other. """ @@ -545,7 +570,7 @@ class LineString(OGRGeometry): def y(self): "Returns the Y coordinates in a list." return self._listarr(capi.gety) - + @property def z(self): "Returns the Z coordinates in a list." @@ -610,7 +635,7 @@ class GeometryCollection(OGRGeometry): raise OGRIndexError('index out of range: %s' % index) else: return OGRGeometry(capi.clone_geom(capi.get_geom_ref(self.ptr, index)), self.srs) - + def __iter__(self): "Iterates over each Geometry." for i in xrange(self.geom_count): @@ -658,5 +683,5 @@ GEO_CLASSES = {1 : Point, 5 : MultiLineString, 6 : MultiPolygon, 7 : GeometryCollection, - 101: LinearRing, + 101: LinearRing, } diff --git a/django/contrib/gis/gdal/prototypes/geom.py b/django/contrib/gis/gdal/prototypes/geom.py index 2898198d0d..e40f33e745 100644 --- a/django/contrib/gis/gdal/prototypes/geom.py +++ b/django/contrib/gis/gdal/prototypes/geom.py @@ -83,7 +83,8 @@ get_geom_srs = srs_output(lgdal.OGR_G_GetSpatialReference, [c_void_p]) get_area = double_output(lgdal.OGR_G_GetArea, [c_void_p]) get_centroid = void_output(lgdal.OGR_G_Centroid, [c_void_p, c_void_p]) get_dims = int_output(lgdal.OGR_G_GetDimension, [c_void_p]) -get_coord_dims = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p]) +get_coord_dim = int_output(lgdal.OGR_G_GetCoordinateDimension, [c_void_p]) +set_coord_dim = void_output(lgdal.OGR_G_SetCoordinateDimension, [c_void_p, c_int], errcheck=False) get_geom_count = int_output(lgdal.OGR_G_GetGeometryCount, [c_void_p]) get_geom_name = const_string_output(lgdal.OGR_G_GetGeometryName, [c_void_p]) diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index c920adc6c0..b5d8046c0e 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -319,6 +319,18 @@ class OGRGeomTest(unittest.TestCase): self.assertAlmostEqual(trans.x, p.x, prec) self.assertAlmostEqual(trans.y, p.y, prec) + def test09c_transform_dim(self): + "Testing coordinate dimension is the same on transformed geometries." + ls_orig = OGRGeometry('LINESTRING(-104.609 38.255)', 4326) + ls_trans = OGRGeometry('LINESTRING(992385.4472045 481455.4944650)', 2774) + + prec = 3 + ls_orig.transform(ls_trans.srs) + # Making sure the coordinate dimension is still 2D. + self.assertEqual(2, ls_orig.coord_dim) + self.assertAlmostEqual(ls_trans.x[0], ls_orig.x[0], prec) + self.assertAlmostEqual(ls_trans.y[0], ls_orig.y[0], prec) + def test10_difference(self): "Testing difference()." for i in xrange(len(topology_geoms)): diff --git a/django/contrib/gis/tests/geoapp/test_regress.py b/django/contrib/gis/tests/geoapp/test_regress.py index 043dace769..e2500308bb 100644 --- a/django/contrib/gis/tests/geoapp/test_regress.py +++ b/django/contrib/gis/tests/geoapp/test_regress.py @@ -28,6 +28,7 @@ class GeoRegressionTests(unittest.TestCase): kmz = render_to_kmz('gis/kml/placemarks.kml', {'places' : places}) @no_spatialite + @no_mysql def test03_extent(self): "Testing `extent` on a table with a single point, see #11827." pnt = City.objects.get(name='Pueblo').point diff --git a/django/contrib/gis/tests/layermap/models.py b/django/contrib/gis/tests/layermap/models.py index 5dbd528030..3a34d16f3f 100644 --- a/django/contrib/gis/tests/layermap/models.py +++ b/django/contrib/gis/tests/layermap/models.py @@ -29,6 +29,20 @@ class Interstate(models.Model): path = models.LineStringField() objects = models.GeoManager() +# Same as `City` above, but for testing model inheritance. +class CityBase(models.Model): + name = models.CharField(max_length=25) + population = models.IntegerField() + density = models.DecimalField(max_digits=7, decimal_places=1) + point = models.PointField() + objects = models.GeoManager() + +class ICity1(CityBase): + dt = models.DateField() + +class ICity2(ICity1): + dt_time = models.DateTimeField(auto_now=True) + # Mapping dictionaries for the models above. co_mapping = {'name' : 'Name', 'state' : {'name' : 'State'}, # ForeignKey's use another mapping dictionary for the _related_ Model (State in this case). diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index 56a9f2bdfe..b17e7b92fc 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -1,7 +1,7 @@ import os, unittest from copy import copy from decimal import Decimal -from models import City, County, CountyFeat, Interstate, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping +from models import City, County, CountyFeat, Interstate, ICity1, ICity2, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping from django.contrib.gis.db.backend import SpatialBackend from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey from django.contrib.gis.gdal import DataSource @@ -242,6 +242,26 @@ class LayerMapTest(unittest.TestCase): lm.save(step=st, strict=True) self.county_helper(county_feat=False) + def test06_model_inheritance(self): + "Tests LayerMapping on inherited models. See #12093." + icity_mapping = {'name' : 'Name', + 'population' : 'Population', + 'density' : 'Density', + 'point' : 'POINT', + 'dt' : 'Created', + } + + # Parent model has geometry field. + lm1 = LayerMapping(ICity1, city_shp, icity_mapping) + lm1.save() + + # Grandparent has geometry field. + lm2 = LayerMapping(ICity2, city_shp, icity_mapping) + lm2.save() + + self.assertEqual(6, ICity1.objects.count()) + self.assertEqual(3, ICity2.objects.count()) + def suite(): s = unittest.TestSuite() s.addTest(unittest.makeSuite(LayerMapTest)) diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index 43bc70a0ec..57c957811d 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -514,16 +514,26 @@ class LayerMapping(object): def geometry_column(self): "Returns the GeometryColumn model associated with the geographic column." from django.contrib.gis.models import GeometryColumns - # Getting the GeometryColumn object. + # Use the `get_field_by_name` on the model's options so that we + # get the correct model if there's model inheritance -- otherwise + # the returned model is None. + opts = self.model._meta + fld, model, direct, m2m = opts.get_field_by_name(self.geom_field) + if model is None: model = self.model + + # Trying to get the `GeometryColumns` object that corresponds to the + # the geometry field. try: - db_table = self.model._meta.db_table - geo_col = self.geom_field + db_table = model._meta.db_table + geo_col = fld.column + if SpatialBackend.oracle: # Making upper case for Oracle. db_table = db_table.upper() geo_col = geo_col.upper() - gc_kwargs = {GeometryColumns.table_name_col() : db_table, - GeometryColumns.geom_col_name() : geo_col, + + gc_kwargs = { GeometryColumns.table_name_col() : db_table, + GeometryColumns.geom_col_name() : geo_col, } return GeometryColumns.objects.get(**gc_kwargs) except Exception, msg: diff --git a/django/core/context_processors.py b/django/core/context_processors.py index cb07125ce7..b950dba0f6 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -8,6 +8,8 @@ RequestContext. """ from django.conf import settings +from django.middleware.csrf import get_token +from django.utils.functional import lazy, memoize, SimpleLazyObject def auth(request): """ @@ -17,17 +19,46 @@ def auth(request): If there is no 'user' attribute in the request, uses AnonymousUser (from django.contrib.auth). """ - if hasattr(request, 'user'): - user = request.user - else: - from django.contrib.auth.models import AnonymousUser - user = AnonymousUser() + # If we access request.user, request.session is accessed, which results in + # 'Vary: Cookie' being sent in every request that uses this context + # processor, which can easily be every request on a site if + # TEMPLATE_CONTEXT_PROCESSORS has this context processor added. This kills + # the ability to cache. So, we carefully ensure these attributes are lazy. + # We don't use django.utils.functional.lazy() for User, because that + # requires knowing the class of the object we want to proxy, which could + # break with custom auth backends. LazyObject is a less complete but more + # flexible solution that is a good enough wrapper for 'User'. + def get_user(): + if hasattr(request, 'user'): + return request.user + else: + from django.contrib.auth.models import AnonymousUser + return AnonymousUser() + return { - 'user': user, - 'messages': user.get_and_delete_messages(), - 'perms': PermWrapper(user), + 'user': SimpleLazyObject(get_user), + 'messages': lazy(memoize(lambda: get_user().get_and_delete_messages(), {}, 0), list)(), + 'perms': lazy(lambda: PermWrapper(get_user()), PermWrapper)(), } +def csrf(request): + """ + Context processor that provides a CSRF token, or the string 'NOTPROVIDED' if + it has not been provided by either a view decorator or the middleware + """ + def _get_val(): + token = get_token(request) + if token is None: + # In order to be able to provide debugging info in the + # case of misconfiguration, we use a sentinel value + # instead of returning an empty dict. + return 'NOTPROVIDED' + else: + return token + _get_val = lazy(_get_val, str) + + return {'csrf_token': _get_val() } + def debug(request): "Returns context variables helpful for debugging." context_extras = {} @@ -79,7 +110,7 @@ class PermWrapper(object): def __getitem__(self, module_name): return PermLookupDict(self.user, module_name) - + def __iter__(self): # I am large, I contain multitudes. raise TypeError("PermWrapper is not iterable.") diff --git a/django/core/files/storage.py b/django/core/files/storage.py index e183c17dd3..96c0b54623 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -118,10 +118,6 @@ class Storage(object): """ raise NotImplementedError() - # Needed by django.utils.functional.LazyObject (via DefaultStorage). - def get_all_members(self): - return self.__members__ - class FileSystemStorage(Storage): """ Standard filesystem storage diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py new file mode 100644 index 0000000000..b02575793d --- /dev/null +++ b/django/core/mail/__init__.py @@ -0,0 +1,110 @@ +""" +Tools for sending email. +""" + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.utils.importlib import import_module + +# Imported for backwards compatibility, and for the sake +# of a cleaner namespace. These symbols used to be in +# django/core/mail.py before the introduction of email +# backends and the subsequent reorganization (See #10355) +from django.core.mail.utils import CachedDnsName, DNS_NAME +from django.core.mail.message import \ + EmailMessage, EmailMultiAlternatives, \ + SafeMIMEText, SafeMIMEMultipart, \ + DEFAULT_ATTACHMENT_MIME_TYPE, make_msgid, \ + BadHeaderError, forbid_multi_line_headers +from django.core.mail.backends.smtp import EmailBackend as _SMTPConnection + +def get_connection(backend=None, fail_silently=False, **kwds): + """Load an e-mail backend and return an instance of it. + + If backend is None (default) settings.EMAIL_BACKEND is used. + + Both fail_silently and other keyword arguments are used in the + constructor of the backend. + """ + path = backend or settings.EMAIL_BACKEND + try: + mod = import_module(path) + except ImportError, e: + raise ImproperlyConfigured(('Error importing email backend %s: "%s"' + % (path, e))) + try: + cls = getattr(mod, 'EmailBackend') + except AttributeError: + raise ImproperlyConfigured(('Module "%s" does not define a ' + '"EmailBackend" class' % path)) + return cls(fail_silently=fail_silently, **kwds) + + +def send_mail(subject, message, from_email, recipient_list, + fail_silently=False, auth_user=None, auth_password=None, + connection=None): + """ + Easy wrapper for sending a single message to a recipient list. All members + of the recipient list will see the other recipients in the 'To' field. + + If auth_user is None, the EMAIL_HOST_USER setting is used. + If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. + + Note: The API for this method is frozen. New code wanting to extend the + functionality should use the EmailMessage class directly. + """ + connection = connection or get_connection(username=auth_user, + password=auth_password, + fail_silently=fail_silently) + return EmailMessage(subject, message, from_email, recipient_list, + connection=connection).send() + + +def send_mass_mail(datatuple, fail_silently=False, auth_user=None, + auth_password=None, connection=None): + """ + Given a datatuple of (subject, message, from_email, recipient_list), sends + each message to each recipient list. Returns the number of e-mails sent. + + If from_email is None, the DEFAULT_FROM_EMAIL setting is used. + If auth_user and auth_password are set, they're used to log in. + If auth_user is None, the EMAIL_HOST_USER setting is used. + If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. + + Note: The API for this method is frozen. New code wanting to extend the + functionality should use the EmailMessage class directly. + """ + connection = connection or get_connection(username=auth_user, + password=auth_password, + fail_silently=fail_silently) + messages = [EmailMessage(subject, message, sender, recipient) + for subject, message, sender, recipient in datatuple] + return connection.send_messages(messages) + + +def mail_admins(subject, message, fail_silently=False, connection=None): + """Sends a message to the admins, as defined by the ADMINS setting.""" + if not settings.ADMINS: + return + EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, + settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS], + connection=connection).send(fail_silently=fail_silently) + + +def mail_managers(subject, message, fail_silently=False, connection=None): + """Sends a message to the managers, as defined by the MANAGERS setting.""" + if not settings.MANAGERS: + return + EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, + settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS], + connection=connection).send(fail_silently=fail_silently) + + +class SMTPConnection(_SMTPConnection): + def __init__(self, *args, **kwds): + import warnings + warnings.warn( + 'mail.SMTPConnection is deprecated; use mail.get_connection() instead.', + DeprecationWarning + ) + super(SMTPConnection, self).__init__(*args, **kwds) diff --git a/django/core/mail/backends/__init__.py b/django/core/mail/backends/__init__.py new file mode 100644 index 0000000000..5973b499b0 --- /dev/null +++ b/django/core/mail/backends/__init__.py @@ -0,0 +1 @@ +# Mail backends shipped with Django. diff --git a/django/core/mail/backends/base.py b/django/core/mail/backends/base.py new file mode 100644 index 0000000000..9a3092849d --- /dev/null +++ b/django/core/mail/backends/base.py @@ -0,0 +1,39 @@ +"""Base email backend class.""" + +class BaseEmailBackend(object): + """ + Base class for email backend implementations. + + Subclasses must at least overwrite send_messages(). + """ + def __init__(self, fail_silently=False, **kwargs): + self.fail_silently = fail_silently + + def open(self): + """Open a network connection. + + This method can be overwritten by backend implementations to + open a network connection. + + It's up to the backend implementation to track the status of + a network connection if it's needed by the backend. + + This method can be called by applications to force a single + network connection to be used when sending mails. See the + send_messages() method of the SMTP backend for a reference + implementation. + + The default implementation does nothing. + """ + pass + + def close(self): + """Close a network connection.""" + pass + + def send_messages(self, email_messages): + """ + Sends one or more EmailMessage objects and returns the number of email + messages sent. + """ + raise NotImplementedError diff --git a/django/core/mail/backends/console.py b/django/core/mail/backends/console.py new file mode 100644 index 0000000000..fa71f3816f --- /dev/null +++ b/django/core/mail/backends/console.py @@ -0,0 +1,37 @@ +""" +Email backend that writes messages to console instead of sending them. +""" +import sys +import threading + +from django.core.mail.backends.base import BaseEmailBackend + +class EmailBackend(BaseEmailBackend): + def __init__(self, *args, **kwargs): + self.stream = kwargs.pop('stream', sys.stdout) + self._lock = threading.RLock() + super(EmailBackend, self).__init__(*args, **kwargs) + + def send_messages(self, email_messages): + """Write all messages to the stream in a thread-safe way.""" + if not email_messages: + return + self._lock.acquire() + try: + # The try-except is nested to allow for + # Python 2.4 support (Refs #12147) + try: + stream_created = self.open() + for message in email_messages: + self.stream.write('%s\n' % message.message().as_string()) + self.stream.write('-'*79) + self.stream.write('\n') + self.stream.flush() # flush after each message + if stream_created: + self.close() + except: + if not self.fail_silently: + raise + finally: + self._lock.release() + return len(email_messages) diff --git a/django/core/mail/backends/dummy.py b/django/core/mail/backends/dummy.py new file mode 100644 index 0000000000..273aa0d88e --- /dev/null +++ b/django/core/mail/backends/dummy.py @@ -0,0 +1,9 @@ +""" +Dummy email backend that does nothing. +""" + +from django.core.mail.backends.base import BaseEmailBackend + +class EmailBackend(BaseEmailBackend): + def send_messages(self, email_messages): + return len(email_messages) diff --git a/django/core/mail/backends/filebased.py b/django/core/mail/backends/filebased.py new file mode 100644 index 0000000000..3f6b99b057 --- /dev/null +++ b/django/core/mail/backends/filebased.py @@ -0,0 +1,59 @@ +"""Email backend that writes messages to a file.""" + +import datetime +import os + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.mail.backends.console import EmailBackend as ConsoleEmailBackend + +class EmailBackend(ConsoleEmailBackend): + def __init__(self, *args, **kwargs): + self._fname = None + if 'file_path' in kwargs: + self.file_path = kwargs.pop('file_path') + else: + self.file_path = getattr(settings, 'EMAIL_FILE_PATH',None) + # Make sure self.file_path is a string. + if not isinstance(self.file_path, basestring): + raise ImproperlyConfigured('Path for saving emails is invalid: %r' % self.file_path) + self.file_path = os.path.abspath(self.file_path) + # Make sure that self.file_path is an directory if it exists. + if os.path.exists(self.file_path) and not os.path.isdir(self.file_path): + raise ImproperlyConfigured('Path for saving email messages exists, but is not a directory: %s' % self.file_path) + # Try to create it, if it not exists. + elif not os.path.exists(self.file_path): + try: + os.makedirs(self.file_path) + except OSError, err: + raise ImproperlyConfigured('Could not create directory for saving email messages: %s (%s)' % (self.file_path, err)) + # Make sure that self.file_path is writable. + if not os.access(self.file_path, os.W_OK): + raise ImproperlyConfigured('Could not write to directory: %s' % self.file_path) + # Finally, call super(). + # Since we're using the console-based backend as a base, + # force the stream to be None, so we don't default to stdout + kwargs['stream'] = None + super(EmailBackend, self).__init__(*args, **kwargs) + + def _get_filename(self): + """Return a unique file name.""" + if self._fname is None: + timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + fname = "%s-%s.log" % (timestamp, abs(id(self))) + self._fname = os.path.join(self.file_path, fname) + return self._fname + + def open(self): + if self.stream is None: + self.stream = open(self._get_filename(), 'a') + return True + return False + + def close(self): + try: + if self.stream is not None: + self.stream.close() + finally: + self.stream = None + diff --git a/django/core/mail/backends/locmem.py b/django/core/mail/backends/locmem.py new file mode 100644 index 0000000000..642bfc49fb --- /dev/null +++ b/django/core/mail/backends/locmem.py @@ -0,0 +1,24 @@ +""" +Backend for test environment. +""" + +from django.core import mail +from django.core.mail.backends.base import BaseEmailBackend + +class EmailBackend(BaseEmailBackend): + """A email backend for use during test sessions. + + The test connection stores email messages in a dummy outbox, + rather than sending them out on the wire. + + The dummy outbox is accessible through the outbox instance attribute. + """ + def __init__(self, *args, **kwargs): + super(EmailBackend, self).__init__(*args, **kwargs) + if not hasattr(mail, 'outbox'): + mail.outbox = [] + + def send_messages(self, messages): + """Redirect messages to the dummy outbox""" + mail.outbox.extend(messages) + return len(messages) diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py new file mode 100644 index 0000000000..63efe438d3 --- /dev/null +++ b/django/core/mail/backends/smtp.py @@ -0,0 +1,106 @@ +"""SMTP email backend class.""" + +import smtplib +import socket +import threading + +from django.conf import settings +from django.core.mail.backends.base import BaseEmailBackend +from django.core.mail.utils import DNS_NAME + +class EmailBackend(BaseEmailBackend): + """ + A wrapper that manages the SMTP network connection. + """ + def __init__(self, host=None, port=None, username=None, password=None, + use_tls=None, fail_silently=False, **kwargs): + super(EmailBackend, self).__init__(fail_silently=fail_silently) + self.host = host or settings.EMAIL_HOST + self.port = port or settings.EMAIL_PORT + self.username = username or settings.EMAIL_HOST_USER + self.password = password or settings.EMAIL_HOST_PASSWORD + if use_tls is None: + self.use_tls = settings.EMAIL_USE_TLS + else: + self.use_tls = use_tls + self.connection = None + self._lock = threading.RLock() + + def open(self): + """ + Ensures we have a connection to the email server. Returns whether or + not a new connection was required (True or False). + """ + if self.connection: + # Nothing to do if the connection is already open. + return False + try: + # If local_hostname is not specified, socket.getfqdn() gets used. + # For performance, we use the cached FQDN for local_hostname. + self.connection = smtplib.SMTP(self.host, self.port, + local_hostname=DNS_NAME.get_fqdn()) + if self.use_tls: + self.connection.ehlo() + self.connection.starttls() + self.connection.ehlo() + if self.username and self.password: + self.connection.login(self.username, self.password) + return True + except: + if not self.fail_silently: + raise + + def close(self): + """Closes the connection to the email server.""" + try: + try: + self.connection.quit() + except socket.sslerror: + # This happens when calling quit() on a TLS connection + # sometimes. + self.connection.close() + except: + if self.fail_silently: + return + raise + finally: + self.connection = None + + def send_messages(self, email_messages): + """ + Sends one or more EmailMessage objects and returns the number of email + messages sent. + """ + if not email_messages: + return + self._lock.acquire() + try: + new_conn_created = self.open() + if not self.connection: + # We failed silently on open(). + # Trying to send would be pointless. + return + num_sent = 0 + for message in email_messages: + sent = self._send(message) + if sent: + num_sent += 1 + if new_conn_created: + self.close() + finally: + self._lock.release() + return num_sent + + def _send(self, email_message): + """A helper method that does the actual sending.""" + if not email_message.recipients(): + return False + try: + self.connection.sendmail(email_message.from_email, + email_message.recipients(), + email_message.message().as_string()) + except: + if not self.fail_silently: + raise + return False + return True diff --git a/django/core/mail.py b/django/core/mail/message.py similarity index 59% rename from django/core/mail.py rename to django/core/mail/message.py index c305699158..14d0017311 100644 --- a/django/core/mail.py +++ b/django/core/mail/message.py @@ -1,21 +1,16 @@ -""" -Tools for sending email. -""" - import mimetypes import os -import smtplib -import socket -import time import random +import time from email import Charset, Encoders from email.MIMEText import MIMEText from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.Header import Header -from email.Utils import formatdate, parseaddr, formataddr +from email.Utils import formatdate, getaddresses, formataddr from django.conf import settings +from django.core.mail.utils import DNS_NAME from django.utils.encoding import smart_str, force_unicode # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from @@ -26,18 +21,10 @@ Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8') # and cannot be guessed). DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream' -# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of -# seconds, which slows down the restart of the server. -class CachedDnsName(object): - def __str__(self): - return self.get_fqdn() - def get_fqdn(self): - if not hasattr(self, '_fqdn'): - self._fqdn = socket.getfqdn() - return self._fqdn +class BadHeaderError(ValueError): + pass -DNS_NAME = CachedDnsName() # Copied from Python standard library, with the following modifications: # * Used cached hostname for performance. @@ -66,8 +53,6 @@ def make_msgid(idstring=None): msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost) return msgid -class BadHeaderError(ValueError): - pass def forbid_multi_line_headers(name, val): """Forbids multi-line headers, to prevent header injection.""" @@ -79,8 +64,7 @@ def forbid_multi_line_headers(name, val): except UnicodeEncodeError: if name.lower() in ('to', 'from', 'cc'): result = [] - for item in val.split(', '): - nm, addr = parseaddr(item) + for nm, addr in getaddresses((val,)): nm = str(Header(nm, settings.DEFAULT_CHARSET)) result.append(formataddr((nm, str(addr)))) val = ', '.join(result) @@ -91,104 +75,18 @@ def forbid_multi_line_headers(name, val): val = Header(val) return name, val + class SafeMIMEText(MIMEText): def __setitem__(self, name, val): name, val = forbid_multi_line_headers(name, val) MIMEText.__setitem__(self, name, val) + class SafeMIMEMultipart(MIMEMultipart): def __setitem__(self, name, val): name, val = forbid_multi_line_headers(name, val) MIMEMultipart.__setitem__(self, name, val) -class SMTPConnection(object): - """ - A wrapper that manages the SMTP network connection. - """ - - def __init__(self, host=None, port=None, username=None, password=None, - use_tls=None, fail_silently=False): - self.host = host or settings.EMAIL_HOST - self.port = port or settings.EMAIL_PORT - self.username = username or settings.EMAIL_HOST_USER - self.password = password or settings.EMAIL_HOST_PASSWORD - self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS - self.fail_silently = fail_silently - self.connection = None - - def open(self): - """ - Ensures we have a connection to the email server. Returns whether or - not a new connection was required (True or False). - """ - if self.connection: - # Nothing to do if the connection is already open. - return False - try: - # If local_hostname is not specified, socket.getfqdn() gets used. - # For performance, we use the cached FQDN for local_hostname. - self.connection = smtplib.SMTP(self.host, self.port, - local_hostname=DNS_NAME.get_fqdn()) - if self.use_tls: - self.connection.ehlo() - self.connection.starttls() - self.connection.ehlo() - if self.username and self.password: - self.connection.login(self.username, self.password) - return True - except: - if not self.fail_silently: - raise - - def close(self): - """Closes the connection to the email server.""" - try: - try: - self.connection.quit() - except socket.sslerror: - # This happens when calling quit() on a TLS connection - # sometimes. - self.connection.close() - except: - if self.fail_silently: - return - raise - finally: - self.connection = None - - def send_messages(self, email_messages): - """ - Sends one or more EmailMessage objects and returns the number of email - messages sent. - """ - if not email_messages: - return - new_conn_created = self.open() - if not self.connection: - # We failed silently on open(). Trying to send would be pointless. - return - num_sent = 0 - for message in email_messages: - sent = self._send(message) - if sent: - num_sent += 1 - if new_conn_created: - self.close() - return num_sent - - def _send(self, email_message): - """A helper method that does the actual sending.""" - if not email_message.recipients(): - return False - try: - self.connection.sendmail(email_message.from_email, - email_message.recipients(), - email_message.message().as_string()) - except: - if not self.fail_silently: - raise - return False - return True class EmailMessage(object): """ @@ -199,14 +97,14 @@ class EmailMessage(object): encoding = None # None => use settings default def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, - connection=None, attachments=None, headers=None): + connection=None, attachments=None, headers=None): """ Initialize a single email message (which can be sent to multiple recipients). - All strings used to create the message can be unicode strings (or UTF-8 - bytestrings). The SafeMIMEText class will handle any necessary encoding - conversions. + All strings used to create the message can be unicode strings + (or UTF-8 bytestrings). The SafeMIMEText class will handle any + necessary encoding conversions. """ if to: assert not isinstance(to, basestring), '"to" argument must be a list or tuple' @@ -226,8 +124,9 @@ class EmailMessage(object): self.connection = connection def get_connection(self, fail_silently=False): + from django.core.mail import get_connection if not self.connection: - self.connection = SMTPConnection(fail_silently=fail_silently) + self.connection = get_connection(fail_silently=fail_silently) return self.connection def message(self): @@ -332,6 +231,7 @@ class EmailMessage(object): filename=filename) return attachment + class EmailMultiAlternatives(EmailMessage): """ A version of EmailMessage that makes it easy to send multipart/alternative @@ -371,56 +271,3 @@ class EmailMultiAlternatives(EmailMessage): for alternative in self.alternatives: msg.attach(self._create_mime_attachment(*alternative)) return msg - -def send_mail(subject, message, from_email, recipient_list, - fail_silently=False, auth_user=None, auth_password=None): - """ - Easy wrapper for sending a single message to a recipient list. All members - of the recipient list will see the other recipients in the 'To' field. - - If auth_user is None, the EMAIL_HOST_USER setting is used. - If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. - - Note: The API for this method is frozen. New code wanting to extend the - functionality should use the EmailMessage class directly. - """ - connection = SMTPConnection(username=auth_user, password=auth_password, - fail_silently=fail_silently) - return EmailMessage(subject, message, from_email, recipient_list, - connection=connection).send() - -def send_mass_mail(datatuple, fail_silently=False, auth_user=None, - auth_password=None): - """ - Given a datatuple of (subject, message, from_email, recipient_list), sends - each message to each recipient list. Returns the number of e-mails sent. - - If from_email is None, the DEFAULT_FROM_EMAIL setting is used. - If auth_user and auth_password are set, they're used to log in. - If auth_user is None, the EMAIL_HOST_USER setting is used. - If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. - - Note: The API for this method is frozen. New code wanting to extend the - functionality should use the EmailMessage class directly. - """ - connection = SMTPConnection(username=auth_user, password=auth_password, - fail_silently=fail_silently) - messages = [EmailMessage(subject, message, sender, recipient) - for subject, message, sender, recipient in datatuple] - return connection.send_messages(messages) - -def mail_admins(subject, message, fail_silently=False): - """Sends a message to the admins, as defined by the ADMINS setting.""" - if not settings.ADMINS: - return - EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, - settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS] - ).send(fail_silently=fail_silently) - -def mail_managers(subject, message, fail_silently=False): - """Sends a message to the managers, as defined by the MANAGERS setting.""" - if not settings.MANAGERS: - return - EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, - settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS] - ).send(fail_silently=fail_silently) diff --git a/django/core/mail/utils.py b/django/core/mail/utils.py new file mode 100644 index 0000000000..322a3a1b79 --- /dev/null +++ b/django/core/mail/utils.py @@ -0,0 +1,19 @@ +""" +Email message and email sending related helper functions. +""" + +import socket + + +# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of +# seconds, which slows down the restart of the server. +class CachedDnsName(object): + def __str__(self): + return self.get_fqdn() + + def get_fqdn(self): + if not hasattr(self, '_fqdn'): + self._fqdn = socket.getfqdn() + return self._fqdn + +DNS_NAME = CachedDnsName() diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index fe51d45bb3..165006efd1 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -57,12 +57,15 @@ class Command(NoArgsCommand): # Create the tables for each model for app in models.get_apps(): app_name = app.__name__.split('.')[-2] - model_list = models.get_models(app) + model_list = models.get_models(app, include_auto_created=True) for model in model_list: # Create the model's database table, if it doesn't already exist. if verbosity >= 2: print "Processing %s.%s model" % (app_name, model._meta.object_name) - if connection.introspection.table_name_converter(model._meta.db_table) in tables: + opts = model._meta + if (connection.introspection.table_name_converter(opts.db_table) in tables or + (opts.auto_created and + connection.introspection.table_name_converter(opts.auto_created._meta.db_table in tables))): continue sql, references = connection.creation.sql_create_model(model, self.style, seen_models) seen_models.add(model) @@ -78,19 +81,6 @@ class Command(NoArgsCommand): cursor.execute(statement) tables.append(connection.introspection.table_name_converter(model._meta.db_table)) - # Create the m2m tables. This must be done after all tables have been created - # to ensure that all referred tables will exist. - for app in models.get_apps(): - app_name = app.__name__.split('.')[-2] - model_list = models.get_models(app) - for model in model_list: - if model in created_models: - sql = connection.creation.sql_for_many_to_many(model, self.style) - if sql: - if verbosity >= 2: - print "Creating many-to-many tables for %s.%s model" % (app_name, model._meta.object_name) - for statement in sql: - cursor.execute(statement) transaction.commit_unless_managed() diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 14fd3f8214..caf40d088d 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -23,7 +23,7 @@ def sql_create(app, style): # We trim models from the current app so that the sqlreset command does not # generate invalid SQL (leaving models out of known_models is harmless, so # we can be conservative). - app_models = models.get_models(app) + app_models = models.get_models(app, include_auto_created=True) final_output = [] tables = connection.introspection.table_names() known_models = set([model for model in connection.introspection.installed_models(tables) if model not in app_models]) @@ -40,10 +40,6 @@ def sql_create(app, style): # Keep track of the fact that we've created the table for this model. known_models.add(model) - # Create the many-to-many join tables. - for model in app_models: - final_output.extend(connection.creation.sql_for_many_to_many(model, style)) - # Handle references to tables that are from other apps # but don't exist physically. not_installed_models = set(pending_references.keys()) @@ -82,7 +78,7 @@ def sql_delete(app, style): to_delete = set() references_to_delete = {} - app_models = models.get_models(app) + app_models = models.get_models(app, include_auto_created=True) for model in app_models: if cursor and connection.introspection.table_name_converter(model._meta.db_table) in table_names: # The table exists, so it needs to be dropped @@ -97,13 +93,6 @@ def sql_delete(app, style): if connection.introspection.table_name_converter(model._meta.db_table) in table_names: output.extend(connection.creation.sql_destroy_model(model, references_to_delete, style)) - # Output DROP TABLE statements for many-to-many tables. - for model in app_models: - opts = model._meta - for f in opts.local_many_to_many: - if cursor and connection.introspection.table_name_converter(f.m2m_db_table()) in table_names: - output.extend(connection.creation.sql_destroy_many_to_many(model, f, style)) - # Close database connection explicitly, in case this output is being piped # directly into a database client, to avoid locking issues. if cursor: diff --git a/django/core/management/validation.py b/django/core/management/validation.py index b971558ac7..97164d75c3 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -79,27 +79,28 @@ def get_validation_errors(outfile, app=None): rel_opts = f.rel.to._meta rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() rel_query_name = f.related_query_name() - for r in rel_opts.fields: - if r.name == rel_name: - e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) - if r.name == rel_query_name: - e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) - for r in rel_opts.local_many_to_many: - if r.name == rel_name: - e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) - if r.name == rel_query_name: - e.add(opts, "Reverse query name for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) - for r in rel_opts.get_all_related_many_to_many_objects(): - if r.get_accessor_name() == rel_name: - e.add(opts, "Accessor for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) - if r.get_accessor_name() == rel_query_name: - e.add(opts, "Reverse query name for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) - for r in rel_opts.get_all_related_objects(): - if r.field is not f: + if not f.rel.is_hidden(): + for r in rel_opts.fields: + if r.name == rel_name: + e.add(opts, "Accessor for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + if r.name == rel_query_name: + e.add(opts, "Reverse query name for field '%s' clashes with field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + for r in rel_opts.local_many_to_many: + if r.name == rel_name: + e.add(opts, "Accessor for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + if r.name == rel_query_name: + e.add(opts, "Reverse query name for field '%s' clashes with m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.name, f.name)) + for r in rel_opts.get_all_related_many_to_many_objects(): if r.get_accessor_name() == rel_name: - e.add(opts, "Accessor for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + e.add(opts, "Accessor for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) if r.get_accessor_name() == rel_query_name: - e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + e.add(opts, "Reverse query name for field '%s' clashes with related m2m field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + for r in rel_opts.get_all_related_objects(): + if r.field is not f: + if r.get_accessor_name() == rel_name: + e.add(opts, "Accessor for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) + if r.get_accessor_name() == rel_query_name: + e.add(opts, "Reverse query name for field '%s' clashes with related field '%s.%s'. Add a related_name argument to the definition for '%s'." % (f.name, rel_opts.object_name, r.get_accessor_name(), f.name)) seen_intermediary_signatures = [] for i, f in enumerate(opts.local_many_to_many): @@ -117,48 +118,80 @@ def get_validation_errors(outfile, app=None): if f.unique: e.add(opts, "ManyToManyFields cannot be unique. Remove the unique argument on '%s'." % f.name) - if getattr(f.rel, 'through', None) is not None: - if hasattr(f.rel, 'through_model'): - from_model, to_model = cls, f.rel.to - if from_model == to_model and f.rel.symmetrical: - e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.") - seen_from, seen_to, seen_self = False, False, 0 - for inter_field in f.rel.through_model._meta.fields: - rel_to = getattr(inter_field.rel, 'to', None) - if from_model == to_model: # relation to self - if rel_to == from_model: - seen_self += 1 - if seen_self > 2: - e.add(opts, "Intermediary model %s has more than two foreign keys to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name)) - else: - if rel_to == from_model: - if seen_from: - e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, from_model._meta.object_name)) - else: - seen_from = True - elif rel_to == to_model: - if seen_to: - e.add(opts, "Intermediary model %s has more than one foreign key to %s, which is ambiguous and is not permitted." % (f.rel.through_model._meta.object_name, rel_to._meta.object_name)) - else: - seen_to = True - if f.rel.through_model not in models.get_models(): - e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed." % (f.name, f.rel.through)) - signature = (f.rel.to, cls, f.rel.through_model) - if signature in seen_intermediary_signatures: - e.add(opts, "The model %s has two manually-defined m2m relations through the model %s, which is not permitted. Please consider using an extra field on your intermediary model instead." % (cls._meta.object_name, f.rel.through_model._meta.object_name)) + if f.rel.through is not None and not isinstance(f.rel.through, basestring): + from_model, to_model = cls, f.rel.to + if from_model == to_model and f.rel.symmetrical and not f.rel.through._meta.auto_created: + e.add(opts, "Many-to-many fields with intermediate tables cannot be symmetrical.") + seen_from, seen_to, seen_self = False, False, 0 + for inter_field in f.rel.through._meta.fields: + rel_to = getattr(inter_field.rel, 'to', None) + if from_model == to_model: # relation to self + if rel_to == from_model: + seen_self += 1 + if seen_self > 2: + e.add(opts, "Intermediary model %s has more than " + "two foreign keys to %s, which is ambiguous " + "and is not permitted." % ( + f.rel.through._meta.object_name, + from_model._meta.object_name + ) + ) else: - seen_intermediary_signatures.append(signature) - seen_related_fk, seen_this_fk = False, False - for field in f.rel.through_model._meta.fields: - if field.rel: - if not seen_related_fk and field.rel.to == f.rel.to: - seen_related_fk = True - elif field.rel.to == cls: - seen_this_fk = True - if not seen_related_fk or not seen_this_fk: - e.add(opts, "'%s' has a manually-defined m2m relation through model %s, which does not have foreign keys to %s and %s" % (f.name, f.rel.through, f.rel.to._meta.object_name, cls._meta.object_name)) + if rel_to == from_model: + if seen_from: + e.add(opts, "Intermediary model %s has more " + "than one foreign key to %s, which is " + "ambiguous and is not permitted." % ( + f.rel.through._meta.object_name, + from_model._meta.object_name + ) + ) + else: + seen_from = True + elif rel_to == to_model: + if seen_to: + e.add(opts, "Intermediary model %s has more " + "than one foreign key to %s, which is " + "ambiguous and is not permitted." % ( + f.rel.through._meta.object_name, + rel_to._meta.object_name + ) + ) + else: + seen_to = True + if f.rel.through not in models.get_models(include_auto_created=True): + e.add(opts, "'%s' specifies an m2m relation through model " + "%s, which has not been installed." % (f.name, f.rel.through) + ) + signature = (f.rel.to, cls, f.rel.through) + if signature in seen_intermediary_signatures: + e.add(opts, "The model %s has two manually-defined m2m " + "relations through the model %s, which is not " + "permitted. Please consider using an extra field on " + "your intermediary model instead." % ( + cls._meta.object_name, + f.rel.through._meta.object_name + ) + ) else: - e.add(opts, "'%s' specifies an m2m relation through model %s, which has not been installed" % (f.name, f.rel.through)) + seen_intermediary_signatures.append(signature) + seen_related_fk, seen_this_fk = False, False + for field in f.rel.through._meta.fields: + if field.rel: + if not seen_related_fk and field.rel.to == f.rel.to: + seen_related_fk = True + elif field.rel.to == cls: + seen_this_fk = True + if not seen_related_fk or not seen_this_fk: + e.add(opts, "'%s' has a manually-defined m2m relation " + "through model %s, which does not have foreign keys " + "to %s and %s" % (f.name, f.rel.through._meta.object_name, + f.rel.to._meta.object_name, cls._meta.object_name) + ) + elif isinstance(f.rel.through, basestring): + e.add(opts, "'%s' specifies an m2m relation through model %s, " + "which has not been installed" % (f.name, f.rel.through) + ) rel_opts = f.rel.to._meta rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index b672e4efc3..7b77804009 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -56,7 +56,7 @@ class Serializer(base.Serializer): self._current[field.name] = smart_unicode(related, strings_only=True) def handle_m2m_field(self, obj, field): - if field.creates_table: + if field.rel.through._meta.auto_created: self._current[field.name] = [smart_unicode(related._get_pk_val(), strings_only=True) for related in getattr(obj, field.name).iterator()] diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 2d74fe28f3..4cde0b039d 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -98,7 +98,7 @@ class Serializer(base.Serializer): serialized as references to the object's PK (i.e. the related *data* is not dumped, just the relation). """ - if field.creates_table: + if field.rel.through._meta.auto_created: self._start_relational_field(field) for relobj in getattr(obj, field.name).iterator(): self.xml.addQuickElement("object", attrs={"pk" : smart_unicode(relobj._get_pk_val())}) @@ -233,4 +233,3 @@ def getInnerText(node): else: pass return u"".join(inner_text) - diff --git a/django/db/models/base.py b/django/db/models/base.py index 6ebe12054f..f8a8f8ae23 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -3,11 +3,6 @@ import types import sys import os from itertools import izip -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback. - import django.db.models.manager # Imported to register signal handler. from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS from django.core import validators @@ -25,7 +20,6 @@ from django.utils.encoding import smart_str, force_unicode, smart_unicode from django.utils.text import get_text_list, capfirst from django.conf import settings - class ModelBase(type): """ Metaclass for all models. @@ -239,7 +233,6 @@ class ModelBase(type): signals.class_prepared.send(sender=cls) - class Model(object): __metaclass__ = ModelBase _deferred = False @@ -303,7 +296,14 @@ class Model(object): if rel_obj is None and field.null: val = None else: - val = kwargs.pop(field.attname, field.get_default()) + try: + val = kwargs.pop(field.attname) + except KeyError: + # This is done with an exception rather than the + # default argument on pop because we don't want + # get_default() to be evaluated, and then not used. + # Refs #12057. + val = field.get_default() else: val = field.get_default() if is_related_object: @@ -355,20 +355,26 @@ class Model(object): only module-level classes can be pickled by the default path. """ data = self.__dict__ - if not self._deferred: - return (self.__class__, (), data) + model = self.__class__ + # The obvious thing to do here is to invoke super().__reduce__() + # for the non-deferred case. Don't do that. + # On Python 2.4, there is something wierd with __reduce__, + # and as a result, the super call will cause an infinite recursion. + # See #10547 and #12121. defers = [] pk_val = None - for field in self._meta.fields: - if isinstance(self.__class__.__dict__.get(field.attname), - DeferredAttribute): - defers.append(field.attname) - if pk_val is None: - # The pk_val and model values are the same for all - # DeferredAttribute classes, so we only need to do this - # once. - obj = self.__class__.__dict__[field.attname] - model = obj.model_ref() + if self._deferred: + for field in self._meta.fields: + if isinstance(self.__class__.__dict__.get(field.attname), + DeferredAttribute): + defers.append(field.attname) + if pk_val is None: + # The pk_val and model values are the same for all + # DeferredAttribute classes, so we only need to do this + # once. + obj = self.__class__.__dict__[field.attname] + model = obj.model_ref() + return (model_unpickle, (model, defers), data) def _get_pk_val(self, meta=None): @@ -431,7 +437,7 @@ class Model(object): else: meta = cls._meta - if origin: + if origin and not meta.auto_created: signals.pre_save.send(sender=origin, instance=self, raw=raw) # If we are in a raw save, save the object exactly as presented. @@ -470,7 +476,7 @@ class Model(object): if pk_set: # Determine whether a record with the primary key already exists. if (force_update or (not force_insert and - manager.filter(pk=pk_val).extra(select={'a': 1}).values('a').order_by())): + manager.filter(pk=pk_val).exists())): # It does already exist, so do an UPDATE. if force_update or non_pks: values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks] @@ -504,7 +510,7 @@ class Model(object): setattr(self, meta.pk.attname, result) transaction.commit_unless_managed() - if origin: + if origin and not meta.auto_created: signals.post_save.send(sender=origin, instance=self, created=(not record_exists), raw=raw) @@ -541,7 +547,12 @@ class Model(object): rel_descriptor = cls.__dict__[rel_opts_name] break else: - raise AssertionError("Should never get here.") + # in the case of a hidden fkey just skip it, it'll get + # processed as an m2m + if not related.field.rel.is_hidden(): + raise AssertionError("Should never get here.") + else: + continue delete_qs = rel_descriptor.delete_manager(self).all() for sub_obj in delete_qs: sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index e0afcdb136..8c82ddb42c 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -58,6 +58,10 @@ def add_lazy_relation(cls, field, relation, operation): # If we can't split, assume a model in current app app_label = cls._meta.app_label model_name = relation + except AttributeError: + # If it doesn't have a split it's actually a model class + app_label = relation._meta.app_label + model_name = relation._meta.object_name # Try to look up the related model, and if it's already loaded resolve the # string right away. If get_model returns None, it means that the related @@ -96,7 +100,7 @@ class RelatedField(object): self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()} other = self.rel.to - if isinstance(other, basestring): + if isinstance(other, basestring) or other._meta.pk is None: def resolve_related_class(field, model, cls): field.rel.to = model field.do_related_class(model, cls) @@ -401,22 +405,22 @@ class ForeignRelatedObjectsDescriptor(object): return manager -def create_many_related_manager(superclass, through=False): +def create_many_related_manager(superclass, rel=False): """Creates a manager that subclasses 'superclass' (which is a Manager) and adds behavior for many-to-many related objects.""" + through = rel.through class ManyRelatedManager(superclass): def __init__(self, model=None, core_filters=None, instance=None, symmetrical=None, - join_table=None, source_col_name=None, target_col_name=None): + join_table=None, source_field_name=None, target_field_name=None): super(ManyRelatedManager, self).__init__() self.core_filters = core_filters self.model = model self.symmetrical = symmetrical self.instance = instance - self.join_table = join_table - self.source_col_name = source_col_name - self.target_col_name = target_col_name + self.source_field_name = source_field_name + self.target_field_name = target_field_name self.through = through - self._pk_val = self.instance._get_pk_val() + self._pk_val = self.instance.pk if self._pk_val is None: raise ValueError("%r instance needs to have a primary key value before a many-to-many relationship can be used." % instance.__class__.__name__) @@ -425,36 +429,37 @@ def create_many_related_manager(superclass, through=False): # If the ManyToMany relation has an intermediary model, # the add and remove methods do not exist. - if through is None: + if rel.through._meta.auto_created: def add(self, *objs): - self._add_items(self.source_col_name, self.target_col_name, *objs) + self._add_items(self.source_field_name, self.target_field_name, *objs) # If this is a symmetrical m2m relation to self, add the mirror entry in the m2m table if self.symmetrical: - self._add_items(self.target_col_name, self.source_col_name, *objs) + self._add_items(self.target_field_name, self.source_field_name, *objs) add.alters_data = True def remove(self, *objs): - self._remove_items(self.source_col_name, self.target_col_name, *objs) + self._remove_items(self.source_field_name, self.target_field_name, *objs) # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table if self.symmetrical: - self._remove_items(self.target_col_name, self.source_col_name, *objs) + self._remove_items(self.target_field_name, self.source_field_name, *objs) remove.alters_data = True def clear(self): - self._clear_items(self.source_col_name) + self._clear_items(self.source_field_name) # If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table if self.symmetrical: - self._clear_items(self.target_col_name) + self._clear_items(self.target_field_name) clear.alters_data = True def create(self, **kwargs): # This check needs to be done here, since we can't later remove this # from the method lookup table, as we do with add and remove. - if through is not None: - raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through + if not rel.through._meta.auto_created: + opts = through._meta + raise AttributeError, "Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) new_obj = super(ManyRelatedManager, self).create(**kwargs) self.add(new_obj) return new_obj @@ -470,41 +475,38 @@ def create_many_related_manager(superclass, through=False): return obj, created get_or_create.alters_data = True - def _add_items(self, source_col_name, target_col_name, *objs): + def _add_items(self, source_field_name, target_field_name, *objs): # join_table: name of the m2m link table - # source_col_name: the PK colname in join_table for the source object - # target_col_name: the PK colname in join_table for the target object + # source_field_name: the PK fieldname in join_table for the source object + # target_col_name: the PK fieldname in join_table for the target object # *objs - objects to add. Either object instances, or primary keys of object instances. # If there aren't any objects, there is nothing to do. + from django.db.models import Model if objs: - from django.db.models.base import Model - # Check that all the objects are of the right type new_ids = set() for obj in objs: if isinstance(obj, self.model): - new_ids.add(obj._get_pk_val()) + new_ids.add(obj.pk) elif isinstance(obj, Model): raise TypeError, "'%s' instance expected" % self.model._meta.object_name else: new_ids.add(obj) - # Add the newly created or already existing objects to the join table. - # First find out which items are already added, to avoid adding them twice - cursor = connection.cursor() - cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \ - (target_col_name, self.join_table, source_col_name, - target_col_name, ",".join(['%s'] * len(new_ids))), - [self._pk_val] + list(new_ids)) - existing_ids = set([row[0] for row in cursor.fetchall()]) + vals = self.through._default_manager.values_list(target_field_name, flat=True) + vals = vals.filter(**{ + source_field_name: self._pk_val, + '%s__in' % target_field_name: new_ids, + }) + vals = set(vals) # Add the ones that aren't there already - for obj_id in (new_ids - existing_ids): - cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ - (self.join_table, source_col_name, target_col_name), - [self._pk_val, obj_id]) - transaction.commit_unless_managed() + for obj_id in (new_ids - vals): + self.through._default_manager.create(**{ + '%s_id' % source_field_name: self._pk_val, + '%s_id' % target_field_name: obj_id, + }) - def _remove_items(self, source_col_name, target_col_name, *objs): + def _remove_items(self, source_field_name, target_field_name, *objs): # source_col_name: the PK colname in join_table for the source object # target_col_name: the PK colname in join_table for the target object # *objs - objects to remove @@ -515,24 +517,20 @@ def create_many_related_manager(superclass, through=False): old_ids = set() for obj in objs: if isinstance(obj, self.model): - old_ids.add(obj._get_pk_val()) + old_ids.add(obj.pk) else: old_ids.add(obj) # Remove the specified objects from the join table - cursor = connection.cursor() - cursor.execute("DELETE FROM %s WHERE %s = %%s AND %s IN (%s)" % \ - (self.join_table, source_col_name, - target_col_name, ",".join(['%s'] * len(old_ids))), - [self._pk_val] + list(old_ids)) - transaction.commit_unless_managed() + self.through._default_manager.filter(**{ + source_field_name: self._pk_val, + '%s__in' % target_field_name: old_ids + }).delete() - def _clear_items(self, source_col_name): + def _clear_items(self, source_field_name): # source_col_name: the PK colname in join_table for the source object - cursor = connection.cursor() - cursor.execute("DELETE FROM %s WHERE %s = %%s" % \ - (self.join_table, source_col_name), - [self._pk_val]) - transaction.commit_unless_managed() + self.through._default_manager.filter(**{ + source_field_name: self._pk_val + }).delete() return ManyRelatedManager @@ -554,17 +552,15 @@ class ManyRelatedObjectsDescriptor(object): # model's default manager. rel_model = self.related.model superclass = rel_model._default_manager.__class__ - RelatedManager = create_many_related_manager(superclass, self.related.field.rel.through) + RelatedManager = create_many_related_manager(superclass, self.related.field.rel) - qn = connection.ops.quote_name manager = RelatedManager( model=rel_model, core_filters={'%s__pk' % self.related.field.name: instance._get_pk_val()}, instance=instance, symmetrical=False, - join_table=qn(self.related.field.m2m_db_table()), - source_col_name=qn(self.related.field.m2m_reverse_name()), - target_col_name=qn(self.related.field.m2m_column_name()) + source_field_name=self.related.field.m2m_reverse_field_name(), + target_field_name=self.related.field.m2m_field_name() ) return manager @@ -573,9 +569,9 @@ class ManyRelatedObjectsDescriptor(object): if instance is None: raise AttributeError, "Manager must be accessed via instance" - through = getattr(self.related.field.rel, 'through', None) - if through is not None: - raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through + if not self.related.field.rel.through._meta.auto_created: + opts = self.related.field.rel.through._meta + raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) manager = self.__get__(instance) manager.clear() @@ -590,6 +586,9 @@ class ReverseManyRelatedObjectsDescriptor(object): # ReverseManyRelatedObjectsDescriptor instance. def __init__(self, m2m_field): self.field = m2m_field + # through is provided so that you have easy access to the through + # model (Book.authors.through) for inlines, etc. + self.through = m2m_field.rel.through def __get__(self, instance, instance_type=None): if instance is None: @@ -599,17 +598,15 @@ class ReverseManyRelatedObjectsDescriptor(object): # model's default manager. rel_model=self.field.rel.to superclass = rel_model._default_manager.__class__ - RelatedManager = create_many_related_manager(superclass, self.field.rel.through) + RelatedManager = create_many_related_manager(superclass, self.field.rel) - qn = connection.ops.quote_name manager = RelatedManager( model=rel_model, core_filters={'%s__pk' % self.field.related_query_name(): instance._get_pk_val()}, instance=instance, symmetrical=(self.field.rel.symmetrical and isinstance(instance, rel_model)), - join_table=qn(self.field.m2m_db_table()), - source_col_name=qn(self.field.m2m_column_name()), - target_col_name=qn(self.field.m2m_reverse_name()) + source_field_name=self.field.m2m_field_name(), + target_field_name=self.field.m2m_reverse_field_name() ) return manager @@ -618,9 +615,9 @@ class ReverseManyRelatedObjectsDescriptor(object): if instance is None: raise AttributeError, "Manager must be accessed via instance" - through = getattr(self.field.rel, 'through', None) - if through is not None: - raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s's Manager instead." % through + if not self.field.rel.through._meta.auto_created: + opts = self.field.rel.through._meta + raise AttributeError, "Cannot set values on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name) manager = self.__get__(instance) manager.clear() @@ -642,6 +639,10 @@ class ManyToOneRel(object): self.multiple = True self.parent_link = parent_link + def is_hidden(self): + "Should the related object be hidden?" + return self.related_name and self.related_name[-1] == '+' + def get_related_field(self): """ Returns the Field in the 'to' object to which this relationship is @@ -673,6 +674,10 @@ class ManyToManyRel(object): self.multiple = True self.through = through + def is_hidden(self): + "Should the related object be hidden?" + return self.related_name and self.related_name[-1] == '+' + def get_related_field(self): """ Returns the field in the to' object to which this relationship is tied @@ -693,7 +698,6 @@ class ForeignKey(RelatedField, Field): assert isinstance(to, basestring), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: assert not to._meta.abstract, "%s cannot define a relation with abstract class %s" % (self.__class__.__name__, to._meta.object_name) - to_field = to_field or to._meta.pk.name kwargs['verbose_name'] = kwargs.get('verbose_name', None) kwargs['rel'] = rel_class(to, to_field, @@ -758,7 +762,12 @@ class ForeignKey(RelatedField, Field): cls._meta.duplicate_targets[self.column] = (target, "o2m") def contribute_to_related_class(self, cls, related): - setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) + # Internal FK's - i.e., those with a related name ending with '+' - + # don't get a related descriptor. + if not self.rel.is_hidden(): + setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) + if self.rel.field_name is None: + self.rel.field_name = cls._meta.pk.name def formfield(self, **kwargs): defaults = { @@ -812,6 +821,51 @@ class OneToOneField(ForeignKey): else: setattr(instance, self.attname, data) +def create_many_to_many_intermediary_model(field, klass): + from django.db import models + managed = True + if isinstance(field.rel.to, basestring) and field.rel.to != RECURSIVE_RELATIONSHIP_CONSTANT: + to = field.rel.to + to_model = field.rel.to + def set_managed(field, model, cls): + field.rel.through._meta.managed = model._meta.managed or cls._meta.managed + add_lazy_relation(klass, field, to_model, set_managed) + elif isinstance(field.rel.to, basestring): + to = klass._meta.object_name + to_model = klass + managed = klass._meta.managed + else: + to = field.rel.to._meta.object_name + to_model = field.rel.to + managed = klass._meta.managed or to_model._meta.managed + name = '%s_%s' % (klass._meta.object_name, field.name) + if field.rel.to == RECURSIVE_RELATIONSHIP_CONSTANT or field.rel.to == klass._meta.object_name: + from_ = 'from_%s' % to.lower() + to = 'to_%s' % to.lower() + else: + from_ = klass._meta.object_name.lower() + to = to.lower() + meta = type('Meta', (object,), { + 'db_table': field._get_m2m_db_table(klass._meta), + 'managed': managed, + 'auto_created': klass, + 'unique_together': (from_, to) + }) + # If the models have been split into subpackages, klass.__module__ + # will be the subpackge, not the models module for the app. (See #12168) + # Compose the actual models module name by stripping the trailing parts + # of the namespace until we find .models + parts = klass.__module__.split('.') + while parts[-1] != 'models': + parts.pop() + module = '.'.join(parts) + # Construct and return the new class. + return type(name, (models.Model,), { + 'Meta': meta, + '__module__': module, + from_: models.ForeignKey(klass, related_name='%s+' % name), + to: models.ForeignKey(to_model, related_name='%s+' % name) + }) class ManyToManyField(RelatedField, Field): def __init__(self, to, **kwargs): @@ -829,10 +883,7 @@ class ManyToManyField(RelatedField, Field): self.db_table = kwargs.pop('db_table', None) if kwargs['rel'].through is not None: - self.creates_table = False assert self.db_table is None, "Cannot specify a db_table if an intermediary model is used." - else: - self.creates_table = True Field.__init__(self, **kwargs) @@ -845,62 +896,45 @@ class ManyToManyField(RelatedField, Field): def _get_m2m_db_table(self, opts): "Function that can be curried to provide the m2m table name for this relation" if self.rel.through is not None: - return self.rel.through_model._meta.db_table + return self.rel.through._meta.db_table elif self.db_table: return self.db_table else: return util.truncate_name('%s_%s' % (opts.db_table, self.name), connection.ops.max_name_length()) - def _get_m2m_column_name(self, related): + def _get_m2m_attr(self, related, attr): "Function that can be curried to provide the source column name for the m2m table" - try: - return self._m2m_column_name_cache - except: - if self.rel.through is not None: - for f in self.rel.through_model._meta.fields: - if hasattr(f,'rel') and f.rel and f.rel.to == related.model: - self._m2m_column_name_cache = f.column - break - # If this is an m2m relation to self, avoid the inevitable name clash - elif related.model == related.parent_model: - self._m2m_column_name_cache = 'from_' + related.model._meta.object_name.lower() + '_id' - else: - self._m2m_column_name_cache = related.model._meta.object_name.lower() + '_id' + 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 diff --git a/django/db/models/loading.py b/django/db/models/loading.py index e07aab4efe..4ab1d5005a 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -131,19 +131,25 @@ class AppCache(object): self._populate() return self.app_errors - def get_models(self, app_mod=None): + def get_models(self, app_mod=None, include_auto_created=False): """ Given a module containing models, returns a list of the models. Otherwise returns a list of all installed models. + + By default, auto-created models (i.e., m2m models without an + explicit intermediate table) are not included. However, if you + specify include_auto_created=True, they will be. """ self._populate() if app_mod: - return self.app_models.get(app_mod.__name__.split('.')[-2], SortedDict()).values() + model_list = self.app_models.get(app_mod.__name__.split('.')[-2], SortedDict()).values() else: model_list = [] for app_entry in self.app_models.itervalues(): model_list.extend(app_entry.values()) - return model_list + if not include_auto_created: + return filter(lambda o: not o._meta.auto_created, model_list) + return model_list def get_model(self, app_label, model_name, seed_cache=True): """ diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 52612d8f64..7487fa0d46 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -1,5 +1,4 @@ import copy - from django.db.models.query import QuerySet, EmptyQuerySet, insert_query from django.db.models import signals from django.db.models.fields import FieldDoesNotExist @@ -173,6 +172,9 @@ class Manager(object): def only(self, *args, **kwargs): return self.get_query_set().only(*args, **kwargs) + def exists(self, *args, **kwargs): + return self.get_query_set().exists(*args, **kwargs) + def _insert(self, values, **kwargs): return insert_query(self.model, values, **kwargs) diff --git a/django/db/models/options.py b/django/db/models/options.py index 34dd2aac34..05ff54a333 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -21,7 +21,7 @@ get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]| DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label', 'db_tablespace', - 'abstract', 'managed', 'proxy') + 'abstract', 'managed', 'proxy', 'auto_created') class Options(object): def __init__(self, meta, app_label=None): @@ -47,6 +47,7 @@ class Options(object): self.proxy_for_model = None self.parents = SortedDict() self.duplicate_targets = {} + self.auto_created = False # To handle various inheritance situations, we need to track where # managers came from (concrete or abstract base classes). @@ -487,4 +488,3 @@ class Options(object): Returns the index of the primary key field in the self.fields list. """ return self.fields.index(self.pk) - diff --git a/django/db/models/query.py b/django/db/models/query.py index 46a86fc03c..36949ac390 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -2,20 +2,13 @@ The main QuerySet implementation. This provides the public API for the ORM. """ -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback - from copy import deepcopy - from django.db import connection, transaction, IntegrityError from django.db.models.aggregates import Aggregate from django.db.models.fields import DateField from django.db.models.query_utils import Q, select_related_descend, CollectedObjects, CyclicDependency, deferred_class_factory from django.db.models import signals, sql - # Used to control how many objects are worked with at once in some cases (e.g. # when deleting objects). CHUNK_SIZE = 100 @@ -444,6 +437,11 @@ class QuerySet(object): return query.execute_sql(None) _update.alters_data = True + def exists(self): + if self._result_cache is None: + return self.query.has_results() + return bool(self._result_cache) + ################################################## # PUBLIC METHODS THAT RETURN A QUERYSET SUBCLASS # ################################################## @@ -1030,7 +1028,8 @@ def delete_objects(seen_objs): # Pre-notify all instances to be deleted. for pk_val, instance in items: - signals.pre_delete.send(sender=cls, instance=instance) + if not cls._meta.auto_created: + signals.pre_delete.send(sender=cls, instance=instance) pk_list = [pk for pk,instance in items] del_query = sql.DeleteQuery(cls, connection) @@ -1064,7 +1063,8 @@ def delete_objects(seen_objs): if field.rel and field.null and field.rel.to in seen_objs: setattr(instance, field.attname, None) - signals.post_delete.send(sender=cls, instance=instance) + if not cls._meta.auto_created: + signals.post_delete.send(sender=cls, instance=instance) setattr(instance, cls._meta.pk.attname, None) if forced_managed: diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 23f99e41ad..7bc45cbce2 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -8,7 +8,6 @@ all about the internals of models in order to get the information it needs. """ from copy import deepcopy - from django.utils.tree import Node from django.utils.datastructures import SortedDict from django.utils.encoding import force_unicode @@ -24,11 +23,6 @@ from django.core.exceptions import FieldError from datastructures import EmptyResultSet, Empty, MultiJoin from constants import * -try: - set -except NameError: - from sets import Set as set # Python 2.3 fallback - __all__ = ['Query', 'BaseQuery'] class BaseQuery(object): @@ -384,6 +378,16 @@ class BaseQuery(object): return number + def has_results(self): + q = self.clone() + q.add_extra({'a': 1}, None, None, None, None, None) + q.add_fields(()) + q.set_extra_mask(('a',)) + q.set_aggregate_mask(()) + q.clear_ordering() + q.set_limits(high=1) + return bool(q.execute_sql(SINGLE)) + def as_sql(self, with_limits=True, with_col_aliases=False): """ Creates the SQL for this query. Returns the SQL string and list of diff --git a/django/forms/models.py b/django/forms/models.py index 6ed8176f5f..11eebb780a 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -259,6 +259,163 @@ class BaseModelForm(BaseForm): return self.cleaned_data + def validate_unique(self): + unique_checks, date_checks = self._get_unique_checks() + form_errors = [] + bad_fields = set() + + field_errors, global_errors = self._perform_unique_checks(unique_checks) + bad_fields.union(field_errors) + form_errors.extend(global_errors) + + field_errors, global_errors = self._perform_date_checks(date_checks) + bad_fields.union(field_errors) + form_errors.extend(global_errors) + + for field_name in bad_fields: + del self.cleaned_data[field_name] + if form_errors: + # Raise the unique together errors since they are considered + # form-wide. + raise ValidationError(form_errors) + + def _get_unique_checks(self): + from django.db.models.fields import FieldDoesNotExist, Field as ModelField + + # Gather a list of checks to perform. We only perform unique checks + # for fields present and not None in cleaned_data. Since this is a + # ModelForm, some fields may have been excluded; we can't perform a unique + # check on a form that is missing fields involved in that check. It also does + # not make sense to check data that didn't validate, and since NULL does not + # equal NULL in SQL we should not do any unique checking for NULL values. + unique_checks = [] + # these are checks for the unique_for_ + date_checks = [] + for check in self.instance._meta.unique_together[:]: + fields_on_form = [field for field in check if self.cleaned_data.get(field) is not None] + if len(fields_on_form) == len(check): + unique_checks.append(check) + + # Gather a list of checks for fields declared as unique and add them to + # the list of checks. Again, skip empty fields and any that did not validate. + for name in self.fields: + try: + f = self.instance._meta.get_field_by_name(name)[0] + except FieldDoesNotExist: + # This is an extra field that's not on the ModelForm, ignore it + continue + if not isinstance(f, ModelField): + # This is an extra field that happens to have a name that matches, + # for example, a related object accessor for this model. So + # get_field_by_name found it, but it is not a Field so do not proceed + # to use it as if it were. + continue + if self.cleaned_data.get(name) is None: + continue + if f.unique: + unique_checks.append((name,)) + if f.unique_for_date and self.cleaned_data.get(f.unique_for_date) is not None: + date_checks.append(('date', name, f.unique_for_date)) + if f.unique_for_year and self.cleaned_data.get(f.unique_for_year) is not None: + date_checks.append(('year', name, f.unique_for_year)) + if f.unique_for_month and self.cleaned_data.get(f.unique_for_month) is not None: + date_checks.append(('month', name, f.unique_for_month)) + return unique_checks, date_checks + + + def _perform_unique_checks(self, unique_checks): + bad_fields = set() + form_errors = [] + + for unique_check in unique_checks: + # Try to look up an existing object with the same values as this + # object's values for all the unique field. + + lookup_kwargs = {} + for field_name in unique_check: + lookup_value = self.cleaned_data[field_name] + # ModelChoiceField will return an object instance rather than + # a raw primary key value, so convert it to a pk value before + # using it in a lookup. + if isinstance(self.fields[field_name], ModelChoiceField): + lookup_value = lookup_value.pk + lookup_kwargs[str(field_name)] = lookup_value + + qs = self.instance.__class__._default_manager.filter(**lookup_kwargs) + + # Exclude the current object from the query if we are editing an + # instance (as opposed to creating a new one) + if self.instance.pk is not None: + qs = qs.exclude(pk=self.instance.pk) + + if qs.exists(): + if len(unique_check) == 1: + self._errors[unique_check[0]] = ErrorList([self.unique_error_message(unique_check)]) + else: + form_errors.append(self.unique_error_message(unique_check)) + + # Mark these fields as needing to be removed from cleaned data + # later. + for field_name in unique_check: + bad_fields.add(field_name) + return bad_fields, form_errors + + def _perform_date_checks(self, date_checks): + bad_fields = set() + for lookup_type, field, unique_for in date_checks: + lookup_kwargs = {} + # there's a ticket to add a date lookup, we can remove this special + # case if that makes it's way in + if lookup_type == 'date': + date = self.cleaned_data[unique_for] + lookup_kwargs['%s__day' % unique_for] = date.day + lookup_kwargs['%s__month' % unique_for] = date.month + lookup_kwargs['%s__year' % unique_for] = date.year + else: + lookup_kwargs['%s__%s' % (unique_for, lookup_type)] = getattr(self.cleaned_data[unique_for], lookup_type) + lookup_kwargs[field] = self.cleaned_data[field] + + qs = self.instance.__class__._default_manager.filter(**lookup_kwargs) + # Exclude the current object from the query if we are editing an + # instance (as opposed to creating a new one) + if self.instance.pk is not None: + qs = qs.exclude(pk=self.instance.pk) + + if qs.exists(): + self._errors[field] = ErrorList([ + self.date_error_message(lookup_type, field, unique_for) + ]) + bad_fields.add(field) + return bad_fields, [] + + def date_error_message(self, lookup_type, field, unique_for): + return _(u"%(field_name)s must be unique for %(date_field)s %(lookup)s.") % { + 'field_name': unicode(self.fields[field].label), + 'date_field': unicode(self.fields[unique_for].label), + 'lookup': lookup_type, + } + + def unique_error_message(self, unique_check): + model_name = capfirst(self.instance._meta.verbose_name) + + # A unique field + if len(unique_check) == 1: + field_name = unique_check[0] + field_label = self.fields[field_name].label + # Insert the error into the error dict, very sneaky + return _(u"%(model_name)s with this %(field_label)s already exists.") % { + 'model_name': unicode(model_name), + 'field_label': unicode(field_label) + } + # unique_together + else: + field_labels = [self.fields[field_name].label for field_name in unique_check] + field_labels = get_text_list(field_labels, _('and')) + return _(u"%(model_name)s with this %(field_label)s already exists.") % { + 'model_name': unicode(model_name), + 'field_label': unicode(field_labels) + } + def save(self, commit=True): """ Saves this ``form``'s cleaned_data into model instance @@ -577,7 +734,7 @@ class BaseInlineFormSet(BaseModelFormSet): save_as_new=False, prefix=None): from django.db.models.fields.related import RelatedObject if instance is None: - self.instance = self.model() + self.instance = self.fk.rel.to() else: self.instance = instance self.save_as_new = save_as_new diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py new file mode 100644 index 0000000000..9ca727fca9 --- /dev/null +++ b/django/middleware/csrf.py @@ -0,0 +1,265 @@ +""" +Cross Site Request Forgery Middleware. + +This module provides a middleware that implements protection +against request forgeries from other sites. +""" + +import itertools +import re +import random + +from django.conf import settings +from django.core.urlresolvers import get_callable +from django.utils.cache import patch_vary_headers +from django.utils.hashcompat import md5_constructor +from django.utils.safestring import mark_safe + +_POST_FORM_RE = \ + re.compile(r'(]*\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 tag plus the added element""" + return mark_safe(match.group() + "
" + \ + "
") + + # Modify any POST forms + response.content, n = _POST_FORM_RE.subn(add_csrf_field, response.content) + if n > 0: + # Content varies with the CSRF cookie, so set the Vary header. + patch_vary_headers(response, ('Cookie',)) + + # Since the content has been modified, any Etag will now be + # incorrect. We could recalculate, but only if we assume that + # the Etag was set by CommonMiddleware. The safest thing is just + # to delete. See bug #9163 + del response['ETag'] + return response + +class CsrfMiddleware(object): + """ + Django middleware that adds protection against Cross Site + Request Forgeries by adding hidden form fields to POST forms and + checking requests for the correct value. + + CsrfMiddleware uses two middleware, CsrfViewMiddleware and + CsrfResponseMiddleware, which can be used independently. It is recommended + to use only CsrfViewMiddleware and use the csrf_token template tag in + templates for inserting the token. + """ + # We can't just inherit from CsrfViewMiddleware and CsrfResponseMiddleware + # because both have process_response methods. + def __init__(self): + self.response_middleware = CsrfResponseMiddleware() + self.view_middleware = CsrfViewMiddleware() + + def process_response(self, request, resp): + # We must do the response post-processing first, because that calls + # get_token(), which triggers a flag saying that the CSRF cookie needs + # to be sent (done in CsrfViewMiddleware.process_response) + resp2 = self.response_middleware.process_response(request, resp) + return self.view_middleware.process_response(request, resp2) + + def process_view(self, request, callback, callback_args, callback_kwargs): + return self.view_middleware.process_view(request, callback, callback_args, + callback_kwargs) + diff --git a/django/template/__init__.py b/django/template/__init__.py index 5493e5bbb7..5b52d36089 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -942,8 +942,14 @@ class Library(object): else: t = get_template(file_name) self.nodelist = t.nodelist - return self.nodelist.render(context_class(dict, - autoescape=context.autoescape)) + new_context = context_class(dict, autoescape=context.autoescape) + # Copy across the CSRF token, if present, because inclusion + # tags are often used for forms, and we need instructions + # for using CSRF protection to be as simple as possible. + csrf_token = context.get('csrf_token', None) + if csrf_token is not None: + new_context['csrf_token'] = csrf_token + return self.nodelist.render(new_context) compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode) compile_func.__doc__ = func.__doc__ diff --git a/django/template/context.py b/django/template/context.py index 1c43387468..f57a3aaa64 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -1,7 +1,12 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module +# Cache of actual callables. _standard_context_processors = None +# We need the CSRF processor no matter what the user has in their settings, +# because otherwise it is a security vulnerability, and we can't afford to leave +# this to human error or failure to read migration instructions. +_builtin_context_processors = ('django.core.context_processors.csrf',) class ContextPopException(Exception): "pop() has been called more times than push()" @@ -75,7 +80,10 @@ def get_standard_processors(): global _standard_context_processors if _standard_context_processors is None: processors = [] - for path in settings.TEMPLATE_CONTEXT_PROCESSORS: + collect = [] + collect.extend(_builtin_context_processors) + collect.extend(settings.TEMPLATE_CONTEXT_PROCESSORS) + for path in collect: i = path.rfind('.') module, attr = path[:i], path[i+1:] try: diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 47e116cd1a..2957c3d045 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -162,7 +162,7 @@ def floatformat(text, arg=-1): try: m = int(d) - d - except (OverflowError, InvalidOperation): + except (ValueError, OverflowError, InvalidOperation): return input_val if not m and p < 0: diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index de746997ab..6d57cdeef8 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -37,6 +37,23 @@ class CommentNode(Node): def render(self, context): return '' +class CsrfTokenNode(Node): + def render(self, context): + csrf_token = context.get('csrf_token', None) + if csrf_token: + if csrf_token == 'NOTPROVIDED': + return mark_safe(u"") + else: + return mark_safe(u"
" % (csrf_token)) + else: + # It's very probable that the token is missing because of + # misconfiguration, so we raise a warning + from django.conf import settings + if settings.DEBUG: + import warnings + warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.") + return u'' + class CycleNode(Node): def __init__(self, cyclevars, variable_name=None): self.cycle_iter = itertools_cycle(cyclevars) @@ -523,6 +540,10 @@ def cycle(parser, token): return node cycle = register.tag(cycle) +def csrf_token(parser, token): + return CsrfTokenNode() +register.tag(csrf_token) + def debug(parser, token): """ Outputs a whole load of debugging information, including the current diff --git a/django/test/client.py b/django/test/client.py index 348bfa7d26..63ad1c1d3a 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -66,6 +66,11 @@ class ClientHandler(BaseHandler): signals.request_started.send(sender=self.__class__) try: request = WSGIRequest(environ) + # sneaky little hack so that we can easily get round + # CsrfViewMiddleware. This makes life easier, and is probably + # required for backwards compatibility with external tests against + # admin views. + request._dont_enforce_csrf_checks = True response = self.get_response(request) # Apply response middleware. @@ -362,12 +367,18 @@ class Client(object): else: post_data = data + # Make `data` into a querystring only if it's not already a string. If + # it is a string, we'll assume that the caller has already encoded it. + query_string = None + if not isinstance(data, basestring): + query_string = urlencode(data, doseq=True) + parsed = urlparse(path) r = { 'CONTENT_LENGTH': len(post_data), 'CONTENT_TYPE': content_type, 'PATH_INFO': urllib.unquote(parsed[2]), - 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], + 'QUERY_STRING': query_string or parsed[4], 'REQUEST_METHOD': 'PUT', 'wsgi.input': FakePayload(post_data), } diff --git a/django/test/utils.py b/django/test/utils.py index d34dd33d15..9d39eee926 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -2,6 +2,7 @@ import sys, time, os from django.conf import settings from django.db import connection from django.core import mail +from django.core.mail.backends import locmem from django.test import signals from django.template import Template from django.utils.translation import deactivate @@ -28,37 +29,22 @@ def instrumented_test_render(self, context): signals.template_rendered.send(sender=self, template=self, context=context) return self.nodelist.render(context) -class TestSMTPConnection(object): - """A substitute SMTP connection for use during test sessions. - The test connection stores email messages in a dummy outbox, - rather than sending them out on the wire. - - """ - def __init__(*args, **kwargs): - pass - def open(self): - "Mock the SMTPConnection open() interface" - pass - def close(self): - "Mock the SMTPConnection close() interface" - pass - def send_messages(self, messages): - "Redirect messages to the dummy outbox" - mail.outbox.extend(messages) - return len(messages) def setup_test_environment(): """Perform any global pre-test setup. This involves: - Installing the instrumented test renderer - - Diverting the email sending functions to a test buffer + - Set the email backend to the locmem email backend. - Setting the active locale to match the LANGUAGE_CODE setting. """ Template.original_render = Template.render Template.render = instrumented_test_render mail.original_SMTPConnection = mail.SMTPConnection - mail.SMTPConnection = TestSMTPConnection + mail.SMTPConnection = locmem.EmailBackend + + mail.original_email_backend = settings.EMAIL_BACKEND + settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem' mail.outbox = [] @@ -77,8 +63,10 @@ def teardown_test_environment(): mail.SMTPConnection = mail.original_SMTPConnection del mail.original_SMTPConnection - del mail.outbox + settings.EMAIL_BACKEND = mail.original_email_backend + del mail.original_email_backend + del mail.outbox def get_runner(settings): test_path = settings.TEST_RUNNER.split('.') diff --git a/django/utils/decorators.py b/django/utils/decorators.py index 4636a2d040..e3f16e462e 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -6,6 +6,19 @@ try: except ImportError: from django.utils.functional import wraps, update_wrapper # Python 2.3, 2.4 fallback. + +# Licence for MethodDecoratorAdaptor and auto_adapt_to_methods +# +# This code is taken from stackoverflow.com [1], the code being supplied by +# users 'Ants Aasma' [2] and 'Silent Ghost' [3] with modifications. It is +# legally included here under the terms of the Creative Commons +# Attribution-Share Alike 2.5 Generic Licence [4] +# +# [1] http://stackoverflow.com/questions/1288498/using-the-same-decorator-with-arguments-with-functions-and-methods +# [2] http://stackoverflow.com/users/107366/ants-aasma +# [3] http://stackoverflow.com/users/12855/silentghost +# [4] http://creativecommons.org/licenses/by-sa/2.5/ + class MethodDecoratorAdaptor(object): """ Generic way of creating decorators that adapt to being diff --git a/django/utils/functional.py b/django/utils/functional.py index 23614d1712..434b6b76c9 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -257,9 +257,8 @@ class LazyObject(object): A wrapper for another class that can be used to delay instantiation of the wrapped class. - This is useful, for example, if the wrapped class needs to use Django - settings at creation time: we want to permit it to be imported without - accessing settings. + By subclassing, you have the opportunity to intercept and alter the + instantiation. If you don't need to do that, use SimpleLazyObject. """ def __init__(self): self._wrapped = None @@ -267,9 +266,6 @@ class LazyObject(object): def __getattr__(self, name): if self._wrapped is None: self._setup() - if name == "__members__": - # Used to implement dir(obj) - return self._wrapped.get_all_members() return getattr(self._wrapped, name) def __setattr__(self, name, value): @@ -287,3 +283,68 @@ class LazyObject(object): """ raise NotImplementedError + # introspection support: + __members__ = property(lambda self: self.__dir__()) + + def __dir__(self): + if self._wrapped is None: + self._setup() + return dir(self._wrapped) + +class SimpleLazyObject(LazyObject): + """ + A lazy object initialised from any function. + + Designed for compound objects of unknown type. For builtins or objects of + known type, use django.utils.functional.lazy. + """ + def __init__(self, func): + """ + Pass in a callable that returns the object to be wrapped. + + If copies are made of the resulting SimpleLazyObject, which can happen + in various circumstances within Django, then you must ensure that the + callable can be safely run more than once and will return the same + value. + """ + self.__dict__['_setupfunc'] = func + # For some reason, we have to inline LazyObject.__init__ here to avoid + # recursion + self._wrapped = None + + def __str__(self): + if self._wrapped is None: self._setup() + return str(self._wrapped) + + def __unicode__(self): + if self._wrapped is None: self._setup() + return unicode(self._wrapped) + + def __deepcopy__(self, memo): + if self._wrapped is None: + # We have to use SimpleLazyObject, not self.__class__, because the + # latter is proxied. + result = SimpleLazyObject(self._setupfunc) + memo[id(self)] = result + return result + else: + import copy + return copy.deepcopy(self._wrapped, memo) + + # Need to pretend to be the wrapped class, for the sake of objects that care + # about this (especially in equality tests) + def __get_class(self): + if self._wrapped is None: self._setup() + return self._wrapped.__class__ + __class__ = property(__get_class) + + def __eq__(self, other): + if self._wrapped is None: self._setup() + return self._wrapped == other + + def __hash__(self): + if self._wrapped is None: self._setup() + return hash(self._wrapped) + + def _setup(self): + self._wrapped = self._setupfunc() diff --git a/django/views/csrf.py b/django/views/csrf.py new file mode 100644 index 0000000000..aa5e25b5b4 --- /dev/null +++ b/django/views/csrf.py @@ -0,0 +1,69 @@ +from django.http import HttpResponseForbidden +from django.template import Context, Template +from django.conf import settings + +# We include the template inline since we need to be able to reliably display +# this error message, especially for the sake of developers, and there isn't any +# other way of making it available independent of what is in the settings file. + +CSRF_FAILRE_TEMPLATE = """ + + + + + 403 Forbidden + + +

403 Forbidden

+

CSRF verification failed. Request aborted.

+ {% if DEBUG %} +

Help

+ {% if reason %} +

Reason given for failure:

+
+    {{ reason }}
+    
+ {% endif %} + +

In general, this can occur when there is a genuine Cross Site Request Forgery, or when + Django's + CSRF mechanism has not been used correctly. For POST forms, you need to + ensure:

+ +
    +
  • The view function uses RequestContext + for the template, instead of Context.
  • + +
  • In the template, there is a {% templatetag openblock %} csrf_token + {% templatetag closeblock %} template tag inside each POST form that + targets an internal URL.
  • + +
  • If you are not using CsrfViewMiddleware, then you must use + csrf_protect on any views that use the csrf_token + template tag, as well as those that accept the POST data.
  • + +
+ +

You're seeing the help section of this page because you have DEBUG = + True in your Django settings file. Change that to False, + and only the initial error message will be displayed.

+ +

You can customize this page using the CSRF_FAILURE_VIEW setting.

+ {% else %} +

More information is available with DEBUG=True.

+ + {% endif %} + + +""" + +def csrf_failure(request, reason=""): + """ + Default view used when request fails CSRF protection + """ + t = Template(CSRF_FAILRE_TEMPLATE) + c = Context({'DEBUG': settings.DEBUG, + 'reason': reason}) + return HttpResponseForbidden(t.render(c), mimetype='text/html') diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py new file mode 100644 index 0000000000..b789872efe --- /dev/null +++ b/django/views/decorators/csrf.py @@ -0,0 +1,47 @@ +from django.middleware.csrf import CsrfViewMiddleware +from django.utils.decorators import decorator_from_middleware +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. + +csrf_protect = decorator_from_middleware(CsrfViewMiddleware) +csrf_protect.__name__ = "csrf_protect" +csrf_protect.__doc__ = """ +This decorator adds CSRF protection in exactly the same way as +CsrfViewMiddleware, but it can be used on a per view basis. Using both, or +using the decorator multiple times, is harmless and efficient. +""" + +def csrf_response_exempt(view_func): + """ + Modifies a view function so that its response is exempt + from the post-processing of the CSRF middleware. + """ + def wrapped_view(*args, **kwargs): + resp = view_func(*args, **kwargs) + resp.csrf_exempt = True + return resp + return wraps(view_func)(wrapped_view) + +def csrf_view_exempt(view_func): + """ + Marks a view function as being exempt from CSRF view protection. + """ + # We could just do view_func.csrf_exempt = True, but decorators + # are nicer if they don't have side-effects, so we return a new + # function. + def wrapped_view(*args, **kwargs): + return view_func(*args, **kwargs) + wrapped_view.csrf_exempt = True + return wraps(view_func)(wrapped_view) + +def csrf_exempt(view_func): + """ + Marks a view function as being exempt from the CSRF checks + and post processing. + + This is the same as using both the csrf_view_exempt and + csrf_response_exempt decorators. + """ + return csrf_response_exempt(csrf_view_exempt(view_func)) diff --git a/docs/faq/install.txt b/docs/faq/install.txt index fb8005c7e7..28a89ccc1f 100644 --- a/docs/faq/install.txt +++ b/docs/faq/install.txt @@ -18,7 +18,7 @@ How do I get started? What are Django's prerequisites? -------------------------------- -Django requires Python_, specifically any version of Python from 2.3 +Django requires Python_, specifically any version of Python from 2.4 through 2.6. No other Python libraries are required for basic Django usage. @@ -42,30 +42,35 @@ PostgreSQL fans, and MySQL_, `SQLite 3`_, and Oracle_ are also supported. .. _`SQLite 3`: http://www.sqlite.org/ .. _Oracle: http://www.oracle.com/ -Do I lose anything by using Python 2.3 versus newer Python versions, such as Python 2.5? ----------------------------------------------------------------------------------------- +Do I lose anything by using Python 2.4 versus newer Python versions, such as Python 2.5 or 2.6? +----------------------------------------------------------------------------------------------- -Not in the core framework. Currently, Django itself officially -supports any version of Python from 2.3 through 2.6, -inclusive. However, some add-on components may require a more recent -Python version; the ``django.contrib.gis`` component, for example, -requires at least Python 2.4, and third-party applications for use -with Django are, of course, free to set their own version -requirements. +Not in the core framework. Currently, Django itself officially supports any +version of Python from 2.4 through 2.6, inclusive. However, newer versions of +Python are often faster, have more features, and are better supported. +Third-party applications for use with Django are, of course, free to set their +own version requirements. -Please note, however, that over the next year or two Django will begin -dropping support for older Python versions as part of a migration -which will end with Django running on Python 3.0 (see next question -for details). So if you're just starting out with Python, it's -recommended that you use the latest 2.x release (currently, Python -2.6). This will let you take advantage of the numerous improvements -and optimizations to the Python language since version 2.3, and will -help ease the process of dropping support for older Python versions on -the road to Python 3.0. +Over the next year or two Django will begin dropping support for older Python +versions as part of a migration which will end with Django running on Python 3 +(see below for details). -Can I use Django with Python 3.0? +All else being equal, we recommend that you use the latest 2.x release +(currently Python 2.6). This will let you take advantage of the numerous +improvements and optimizations to the Python language since version 2.4, and +will help ease the process of dropping support for older Python versions on +the road to Python 3. + +Can I use Django with Python 2.3? --------------------------------- +Django 1.1 (and earlier) supported Python 2.3. Django 1.2 and newer does not. +We highly recommend you upgrade Python if at all possible, but Django 1.1 will +continue to work on Python 2.3. + +Can I use Django with Python 3? +------------------------------- + Not at the moment. Python 3.0 introduced a number of backwards-incompatible changes to the Python language, and although these changes are generally a good thing for Python's future, it will diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 7326532ec9..803c3140c7 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -148,7 +148,6 @@ Joseph Kocherhans .. _brian rosner: http://oebfare.com/ .. _eldarion: http://eldarion.com/ -.. _pinax: http://pinaxproject.com/ .. _django dose: http://djangodose.com/ `Gary Wilson`_ @@ -189,6 +188,18 @@ Karen Tracey Karen lives in Apex, NC, USA. +`Jannis Leidel`_ + Jannis graduated in media design from `Bauhaus-University Weimar`_, + is the author of a number of pluggable Django apps and likes to + contribute to Open Source projects like Pinax_. He currently works as + a freelance web developer and designer. + + Jannis lives in Berlin, Germany. + +.. _Jannis Leidel: http://jezdez.com/ +.. _Bauhaus-University Weimar: http://www.uni-weimar.de/ +.. _pinax: http://pinaxproject.com/ + Specialists ----------- diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 7e7f4c6338..480b527d6b 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -13,6 +13,21 @@ their deprecation, as per the :ref:`Django deprecation policy hooking up admin URLs. This has been deprecated since the 1.1 release. + * 1.4 + * ``CsrfResponseMiddleware``. This has been deprecated since the 1.2 + release, in favour of the template tag method for inserting the CSRF + token. ``CsrfMiddleware``, which combines ``CsrfResponseMiddleware`` + and ``CsrfViewMiddleware``, is also deprecated. + + * The old imports for CSRF functionality (``django.contrib.csrf.*``), + which moved to core in 1.2, will be removed. + + * ``SMTPConnection``. The 1.2 release deprecated the ``SMTPConnection`` + class in favor of a generic E-mail backend API. + + * The many to many SQL generation functions on the database backends + will be removed. These have been deprecated since the 1.2 release. + * 2.0 * ``django.views.defaults.shortcut()``. This function has been moved to ``django.contrib.contenttypes.views.shortcut()`` as part of the diff --git a/docs/internals/release-process.txt b/docs/internals/release-process.txt index 6d4ad9e8c9..e990ab8ab6 100644 --- a/docs/internals/release-process.txt +++ b/docs/internals/release-process.txt @@ -56,7 +56,7 @@ These releases will contain new features, improvements to existing features, and such. A minor release may deprecate certain features from previous releases. If a feature in version ``A.B`` is deprecated, it will continue to work in version ``A.B+1``. In version ``A.B+2``, use of the feature will raise a -``PendingDeprecationWarning`` but will continue to work. Version ``A.B+3`` will +``DeprecationWarning`` but will continue to work. Version ``A.B+3`` will remove the feature entirely. So, for example, if we decided to remove a function that existed in Django 1.0: diff --git a/docs/intro/install.txt b/docs/intro/install.txt index 237c208f2a..d0776a6ea3 100644 --- a/docs/intro/install.txt +++ b/docs/intro/install.txt @@ -12,7 +12,7 @@ Install Python -------------- Being a Python Web framework, Django requires Python. It works with any Python -version from 2.3 to 2.6 (due to backwards +version from 2.4 to 2.6 (due to backwards incompatibilities in Python 3.0, Django does not currently work with Python 3.0; see :ref:`the Django FAQ ` for more information on supported Python versions and the 3.0 transition), but we recommend installing Python 2.5 or later. If you do so, you won't need to set up a database just yet: Python 2.5 or later includes a lightweight database called SQLite_. diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index 69c8d0f3db..afda1f28a2 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -281,6 +281,7 @@ That'll create a directory :file:`polls`, which is laid out like this:: polls/ __init__.py models.py + tests.py views.py This directory structure will house the poll application. diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index 203c945c02..ad1bd9d990 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -34,11 +34,11 @@ activate the admin site for your installation, do these three things: * Run ``python manage.py syncdb``. Since you have added a new application to :setting:`INSTALLED_APPS`, the database tables need to be updated. - * Edit your ``mysite/urls.py`` file and uncomment the lines below the - "Uncomment the next two lines..." comment. This file is a URLconf; - we'll dig into URLconfs in the next tutorial. For now, all you need to - know is that it maps URL roots to applications. In the end, you should - have a ``urls.py`` file that looks like this: + * Edit your ``mysite/urls.py`` file and uncomment the lines that reference + the admin -- there are three lines in total to uncomment. This file is a + URLconf; we'll dig into URLconfs in the next tutorial. For now, all you + need to know is that it maps URL roots to applications. In the end, you + should have a ``urls.py`` file that looks like this: .. versionchanged:: 1.1 The method for adding admin urls has changed in Django 1.1. diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 238dc63f71..1438a9e776 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -171,15 +171,23 @@ and put the following Python code in it:: This is the simplest view possible. Go to "/polls/" in your browser, and you should see your text. -Now add the following view. It's slightly different, because it takes an -argument (which, remember, is passed in from whatever was captured by the -regular expression in the URLconf):: +Now lets add a few more views. These views are slightly different, because +they take an argument (which, remember, is passed in from whatever was +captured by the regular expression in the URLconf):: def detail(request, poll_id): return HttpResponse("You're looking at poll %s." % poll_id) -Take a look in your browser, at "/polls/34/". It'll display whatever ID you -provide in the URL. + def results(request, poll_id): + return HttpResponse("You're looking at the results of poll %s." % poll_id) + + def vote(request, poll_id): + return HttpResponse("You're voting on poll %s." % poll_id) + +Take a look in your browser, at "/polls/34/". It'll run the `detail()` method +and display whatever ID you provide in the URL. Try "/polls/34/results/" and +"/polls/34/vote/" too -- these will display the placeholder results and voting +pages. Write views that actually do something ====================================== @@ -467,10 +475,10 @@ Copy the file ``mysite/urls.py`` to ``mysite/polls/urls.py``. Then, change ``mysite/urls.py`` to remove the poll-specific URLs and insert an :func:`~django.conf.urls.defaults.include`:: - ... + # ... urlpatterns = patterns('', (r'^polls/', include('mysite.polls.urls')), - ... + # ... :func:`~django.conf.urls.defaults.include`, simply, references another URLconf. Note that the regular expression doesn't have a ``$`` (end-of-string match diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 28ace85ca8..394fc25ea8 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -21,6 +21,7 @@ tutorial, so that the template contains an HTML ```` element: {% if error_message %}

{{ error_message }}

{% endif %} + {% csrf_token %} {% for choice in poll.choice_set.all %}
@@ -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 `. + Now, let's create a Django view that handles the submitted data and does something with it. Remember, in :ref:`Tutorial 3 `, we created a URLconf for the polls application that includes this line:: (r'^(?P\d+)/vote/$', 'vote'), -So let's create a ``vote()`` function in ``mysite/polls/views.py``:: +We also created a dummy implementation of the ``vote()`` function. Let's +create a real version. Add the following to ``mysite/polls/views.py``:: from django.shortcuts import get_object_or_404, render_to_response - from django.http import HttpResponseRedirect + from django.http import HttpResponseRedirect, HttpResponse from django.core.urlresolvers import reverse + from django.template import RequestContext from mysite.polls.models import Choice, Poll # ... def vote(request, poll_id): @@ -68,7 +93,7 @@ So let's create a ``vote()`` function in ``mysite/polls/views.py``:: return render_to_response('polls/detail.html', { 'poll': p, 'error_message': "You didn't select a choice.", - }) + }, context_instance=RequestContext(request)) else: selected_choice.votes += 1 selected_choice.save() diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index c1e05eda1d..0f746bf01b 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -770,7 +770,7 @@ documented in :ref:`topics-http-urls`:: However, the ``self.my_view`` function registered above suffers from two problems: - * It will *not* perform and permission checks, so it will be accessible to + * It will *not* perform any permission checks, so it will be accessible to the general public. * It will *not* provide any header details to prevent caching. This means if the page retrieves data from the database, and caching middleware is @@ -1048,16 +1048,70 @@ automatically:: FriendshipInline, ] +Working with Many-to-Many Models +-------------------------------- + +.. versionadded:: 1.2 + +By default, admin widgets for many-to-many relations will be displayed +on whichever model contains the actual reference to the ``ManyToManyField``. +Depending on your ``ModelAdmin`` definition, each many-to-many field in your +model will be represented by a standard HTML ``